Skip to main content

dvb_ci/ci_ext/
status_query.rs

1//! Status Query objects + audience-metering item structures — ETSI TS 101 699
2//! V1.1.1 §6.2, Tables 35-51 (PDF pp. 42-50). See `docs/ci_plus/status-query.md`.
3//!
4//! Resource ID `0x00211ii1` (`ii` = Module ID, `type = 1*`). The module
5//! interrogates the host's status items (Table 36): Selection Information (1),
6//! Port Profile (2), Viewed Service (3), Activation Status (4).
7//!
8//! APDU objects (Tables 37-41):
9//! - `StatusQueryReq` (`9F 80 00`) — module → host: a 32-bit `StatusItem`.
10//! - `TrapReq` (`9F 80 01`) — module → host: a 32-bit `StatusItem` to monitor.
11//! - `GetNextItemReq` (`9F 80 02`) — module → host: a 32-bit `StartStatusItem`.
12//! - `GetNextItemAck` (`9F 80 03`) — host → module: a 32-bit `NextStatusItem`.
13//! - `StatusAck` (`9F 80 04`) — host → module: the `StatusItem` + its
14//!   `StatusBytes` (format depends on the item — Tables 43/48/49/50).
15//!
16//! The `StatusBytes` of a [`StatusAck`] are carried as an opaque `&[u8]`; the
17//! audience-metering content structures ([`SelectionInformation`],
18//! [`PortProfile`], [`ViewedService`], [`ActivationStatus`]) implement
19//! `Parse`/`Serialize` over **just those bytes** (no `apdu_tag`) so a caller can
20//! decode them once the `StatusItem` identifies the format.
21
22use crate::error::{Error, Result};
23use crate::objects;
24use alloc::vec::Vec;
25use broadcast_common::{Parse, Serialize};
26
27/// Resource-scoped `apdu_tag`s for Status Query (Tables 37-41).
28pub mod tag {
29    use crate::tag::ApduTag;
30    /// `StatusQueryReqTag` = `9F 80 00`.
31    pub const STATUS_QUERY_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x00);
32    /// `TrapReqTag` = `9F 80 01`.
33    pub const TRAP_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x01);
34    /// `GetNextItemReqTag` = `9F 80 02`.
35    pub const GET_NEXT_ITEM_REQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x02);
36    /// `GetNextItemAckTag` = `9F 80 03`.
37    pub const GET_NEXT_ITEM_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x03);
38    /// `StatusAckTag` = `9F 80 04`.
39    pub const STATUS_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x04);
40}
41
42/// Width of the `StatusItem` / `StartStatusItem` / `NextStatusItem` field (32 bits).
43const STATUS_ITEM_LEN: usize = 4;
44
45fn read_u32(b: &[u8]) -> u32 {
46    u32::from_be_bytes([b[0], b[1], b[2], b[3]])
47}
48
49/// `StatusItem` (Table 36) — the host status item a query refers to.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize))]
52#[non_exhaustive]
53pub enum StatusItem {
54    /// `1` — Selection Information (Audience Metering; Table 43).
55    SelectionInformation,
56    /// `2` — Port Profile (Audience Metering; Table 48).
57    PortProfile,
58    /// `3` — Viewed Service (Table 49).
59    ViewedService,
60    /// `4` — Activation Status (Table 50).
61    ActivationStatus,
62    /// `0` and `> 4` — reserved.
63    Reserved(u32),
64}
65
66impl StatusItem {
67    /// Decode a `StatusItem` value.
68    #[must_use]
69    pub fn from_u32(v: u32) -> Self {
70        match v {
71            1 => Self::SelectionInformation,
72            2 => Self::PortProfile,
73            3 => Self::ViewedService,
74            4 => Self::ActivationStatus,
75            other => Self::Reserved(other),
76        }
77    }
78    /// Wire value.
79    #[must_use]
80    pub const fn to_u32(self) -> u32 {
81        match self {
82            Self::SelectionInformation => 1,
83            Self::PortProfile => 2,
84            Self::ViewedService => 3,
85            Self::ActivationStatus => 4,
86            Self::Reserved(v) => v,
87        }
88    }
89    /// Spec token, or `"reserved"`.
90    #[must_use]
91    pub fn name(&self) -> &'static str {
92        match self {
93            Self::SelectionInformation => "Selection Information",
94            Self::PortProfile => "Port Profile",
95            Self::ViewedService => "Viewed Service",
96            Self::ActivationStatus => "Activation Status",
97            Self::Reserved(_) => "reserved",
98        }
99    }
100}
101broadcast_common::impl_spec_display!(StatusItem, Reserved);
102
103// =================== APDU objects ===================
104
105/// `StatusQueryReq()` (Table 37): module → host.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107#[cfg_attr(feature = "serde", derive(serde::Serialize))]
108pub struct StatusQueryReq {
109    /// `StatusItem` queried.
110    pub status_item: StatusItem,
111}
112
113/// `TrapReq()` (Table 38): module → host.
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize))]
116pub struct TrapReq {
117    /// `StatusItem` to monitor for changes.
118    pub status_item: StatusItem,
119}
120
121/// `GetNextItemReq()` (Table 39): module → host.
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123#[cfg_attr(feature = "serde", derive(serde::Serialize))]
124pub struct GetNextItemReq {
125    /// `StartStatusItem` — search start point (raw 32-bit; not necessarily a
126    /// supported item).
127    pub start_status_item: u32,
128}
129
130/// `GetNextItemAck()` (Table 40): host → module.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132#[cfg_attr(feature = "serde", derive(serde::Serialize))]
133pub struct GetNextItemAck {
134    /// `NextStatusItem` — first supported item greater than `StartStatusItem`
135    /// (raw 32-bit; `0` = no higher item).
136    pub next_status_item: u32,
137}
138
139/// `StatusAck()` (Table 41): host → module. The `StatusBytes` are an opaque
140/// byte string whose format depends on `status_item` (Tables 43/48/49/50); an
141/// empty `status_bytes` means the host does not support the item.
142#[derive(Debug, Clone, PartialEq, Eq)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize))]
144pub struct StatusAck<'a> {
145    /// `StatusItem` this reply answers.
146    pub status_item: StatusItem,
147    /// `StatusBytes` — item-dependent payload.
148    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
149    pub status_bytes: &'a [u8],
150}
151
152macro_rules! status_item_object {
153    ($ty:ident, $tag:expr, $what:literal, $field:ident, $decode:expr, $encode:expr) => {
154        impl<'a> Parse<'a> for $ty {
155            type Error = Error;
156            fn parse(bytes: &'a [u8]) -> Result<Self> {
157                let body = objects::parse_apdu_header(bytes, $tag, $what)?;
158                if body.len() < STATUS_ITEM_LEN {
159                    return Err(Error::BufferTooShort {
160                        need: STATUS_ITEM_LEN,
161                        have: body.len(),
162                        what: $what,
163                    });
164                }
165                let raw = read_u32(body);
166                Ok(Self {
167                    $field: $decode(raw),
168                })
169            }
170        }
171        impl Serialize for $ty {
172            type Error = Error;
173            fn serialized_len(&self) -> usize {
174                objects::apdu_len(STATUS_ITEM_LEN)
175            }
176            fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
177                let pos = objects::write_apdu_header($tag, STATUS_ITEM_LEN, buf)?;
178                let raw: u32 = $encode(self.$field);
179                buf[pos..pos + STATUS_ITEM_LEN].copy_from_slice(&raw.to_be_bytes());
180                Ok(pos + STATUS_ITEM_LEN)
181            }
182        }
183    };
184}
185
186status_item_object!(
187    StatusQueryReq,
188    tag::STATUS_QUERY_REQ,
189    "StatusQueryReq",
190    status_item,
191    StatusItem::from_u32,
192    StatusItem::to_u32
193);
194status_item_object!(
195    TrapReq,
196    tag::TRAP_REQ,
197    "TrapReq",
198    status_item,
199    StatusItem::from_u32,
200    StatusItem::to_u32
201);
202status_item_object!(
203    GetNextItemReq,
204    tag::GET_NEXT_ITEM_REQ,
205    "GetNextItemReq",
206    start_status_item,
207    core::convert::identity,
208    core::convert::identity
209);
210status_item_object!(
211    GetNextItemAck,
212    tag::GET_NEXT_ITEM_ACK,
213    "GetNextItemAck",
214    next_status_item,
215    core::convert::identity,
216    core::convert::identity
217);
218
219impl<'a> Parse<'a> for StatusAck<'a> {
220    type Error = Error;
221    fn parse(bytes: &'a [u8]) -> Result<Self> {
222        let body = objects::parse_apdu_header(bytes, tag::STATUS_ACK, "StatusAck")?;
223        if body.len() < STATUS_ITEM_LEN {
224            return Err(Error::BufferTooShort {
225                need: STATUS_ITEM_LEN,
226                have: body.len(),
227                what: "StatusAck",
228            });
229        }
230        Ok(Self {
231            status_item: StatusItem::from_u32(read_u32(body)),
232            status_bytes: &body[STATUS_ITEM_LEN..],
233        })
234    }
235}
236impl Serialize for StatusAck<'_> {
237    type Error = Error;
238    fn serialized_len(&self) -> usize {
239        objects::apdu_len(STATUS_ITEM_LEN + self.status_bytes.len())
240    }
241    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
242        let body_len = STATUS_ITEM_LEN + self.status_bytes.len();
243        let mut pos = objects::write_apdu_header(tag::STATUS_ACK, body_len, buf)?;
244        buf[pos..pos + STATUS_ITEM_LEN].copy_from_slice(&self.status_item.to_u32().to_be_bytes());
245        pos += STATUS_ITEM_LEN;
246        buf[pos..pos + self.status_bytes.len()].copy_from_slice(self.status_bytes);
247        Ok(pos + self.status_bytes.len())
248    }
249}
250
251// =================== Audience-metering item content structures ===================
252//
253// These decode the `StatusBytes` of a StatusAck for items 1-4. They have no
254// apdu_tag/length_field of their own (the StatusAck supplies length); their
255// Parse consumes the whole input slice and Serialize emits exactly the wire
256// bytes, so a round-trip is byte-exact.
257
258/// Width of the `time` field of Selection Information (40 bits = 5 bytes, UTC
259/// time coded as the DVB SI Time and Date Table).
260pub const SELECTION_TIME_LEN: usize = 5;
261
262/// An output entry within a Selection Information [`InPortDescription`] (Table 43).
263#[derive(Debug, Clone, PartialEq, Eq, Default)]
264#[cfg_attr(feature = "serde", derive(serde::Serialize))]
265pub struct OutputSignal<'a> {
266    /// `out_port_id` (Table 46).
267    pub out_port_id: u8,
268    /// `out_signal_desc` — block describing the output signal (opaque; format
269    /// depends on the output port — Table 47).
270    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
271    pub out_signal_desc: &'a [u8],
272}
273
274/// One `in_port_id` block of Selection Information (Table 43): an input signal
275/// source and the list of outputs it is routed to.
276#[derive(Debug, Clone, PartialEq, Eq, Default)]
277#[cfg_attr(feature = "serde", derive(serde::Serialize))]
278pub struct InPortDescription<'a> {
279    /// `in_port_id` (Table 44).
280    pub in_port_id: u8,
281    /// `in_signal_desc` — block describing the input signal (opaque; format
282    /// depends on the input port — Table 45).
283    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
284    pub in_signal_desc: &'a [u8],
285    /// The output signals this input is routed to (`length_outputs` bytes total).
286    pub outputs: Vec<OutputSignal<'a>>,
287}
288
289/// `Selection information status data` (Table 43) — status item 1: a timestamp
290/// and a list of input-port → output-port routing descriptions.
291#[derive(Debug, Clone, PartialEq, Eq, Default)]
292#[cfg_attr(feature = "serde", derive(serde::Serialize))]
293pub struct SelectionInformation<'a> {
294    /// `time` — 5-byte UTC time (DVB SI TDT coding).
295    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
296    pub time: &'a [u8],
297    /// One entry per input port present in the object.
298    pub ports: Vec<InPortDescription<'a>>,
299}
300
301impl<'a> Parse<'a> for SelectionInformation<'a> {
302    type Error = Error;
303    fn parse(body: &'a [u8]) -> Result<Self> {
304        if body.len() < SELECTION_TIME_LEN {
305            return Err(Error::BufferTooShort {
306                need: SELECTION_TIME_LEN,
307                have: body.len(),
308                what: "SelectionInformation.time",
309            });
310        }
311        let time = &body[..SELECTION_TIME_LEN];
312        let mut rest = &body[SELECTION_TIME_LEN..];
313        let mut ports = Vec::new();
314        while !rest.is_empty() {
315            // in_port_id(1) + length_in_signal_desc(1).
316            if rest.len() < 2 {
317                return Err(Error::BufferTooShort {
318                    need: 2,
319                    have: rest.len(),
320                    what: "SelectionInformation.in_port",
321                });
322            }
323            let in_port_id = rest[0];
324            let in_len = rest[1] as usize;
325            let after_in = 2 + in_len;
326            if rest.len() < after_in + 2 {
327                return Err(Error::BufferTooShort {
328                    need: after_in + 2,
329                    have: rest.len(),
330                    what: "SelectionInformation.in_signal_desc",
331                });
332            }
333            let in_signal_desc = &rest[2..after_in];
334            // reserved(4) + length_outputs(12) packed into 2 bytes.
335            let length_outputs =
336                (((rest[after_in] & 0x0F) as usize) << 8) | rest[after_in + 1] as usize;
337            let outputs_start = after_in + 2;
338            let outputs_end = outputs_start + length_outputs;
339            if rest.len() < outputs_end {
340                return Err(Error::BufferTooShort {
341                    need: outputs_end,
342                    have: rest.len(),
343                    what: "SelectionInformation.outputs",
344                });
345            }
346            let mut out_rest = &rest[outputs_start..outputs_end];
347            let mut outputs = Vec::new();
348            while !out_rest.is_empty() {
349                // out_port_id(1) + length_out_signal_desc(1).
350                if out_rest.len() < 2 {
351                    return Err(Error::BufferTooShort {
352                        need: 2,
353                        have: out_rest.len(),
354                        what: "SelectionInformation.out_port",
355                    });
356                }
357                let out_port_id = out_rest[0];
358                let out_len = out_rest[1] as usize;
359                let out_end = 2 + out_len;
360                if out_rest.len() < out_end {
361                    return Err(Error::BufferTooShort {
362                        need: out_end,
363                        have: out_rest.len(),
364                        what: "SelectionInformation.out_signal_desc",
365                    });
366                }
367                outputs.push(OutputSignal {
368                    out_port_id,
369                    out_signal_desc: &out_rest[2..out_end],
370                });
371                out_rest = &out_rest[out_end..];
372            }
373            ports.push(InPortDescription {
374                in_port_id,
375                in_signal_desc,
376                outputs,
377            });
378            rest = &rest[outputs_end..];
379        }
380        Ok(Self { time, ports })
381    }
382}
383impl Serialize for SelectionInformation<'_> {
384    type Error = Error;
385    fn serialized_len(&self) -> usize {
386        let mut n = SELECTION_TIME_LEN;
387        for p in &self.ports {
388            n += 2 + p.in_signal_desc.len() + 2;
389            for o in &p.outputs {
390                n += 2 + o.out_signal_desc.len();
391            }
392        }
393        n
394    }
395    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
396        let total = self.serialized_len();
397        if buf.len() < total {
398            return Err(Error::OutputBufferTooSmall {
399                need: total,
400                have: buf.len(),
401            });
402        }
403        if self.time.len() != SELECTION_TIME_LEN {
404            return Err(Error::InvalidObject {
405                what: "SelectionInformation",
406                reason: "time must be exactly 5 bytes",
407            });
408        }
409        let mut pos = 0;
410        buf[pos..pos + SELECTION_TIME_LEN].copy_from_slice(self.time);
411        pos += SELECTION_TIME_LEN;
412        for p in &self.ports {
413            if p.in_signal_desc.len() > u8::MAX as usize {
414                return Err(Error::InvalidObject {
415                    what: "SelectionInformation",
416                    reason: "in_signal_desc longer than 255 bytes",
417                });
418            }
419            buf[pos] = p.in_port_id;
420            buf[pos + 1] = p.in_signal_desc.len() as u8;
421            pos += 2;
422            buf[pos..pos + p.in_signal_desc.len()].copy_from_slice(p.in_signal_desc);
423            pos += p.in_signal_desc.len();
424            // length_outputs = total bytes of the output entries.
425            let mut length_outputs = 0usize;
426            for o in &p.outputs {
427                length_outputs += 2 + o.out_signal_desc.len();
428            }
429            if length_outputs > 0x0FFF {
430                return Err(Error::InvalidObject {
431                    what: "SelectionInformation",
432                    reason: "length_outputs exceeds 12-bit maximum",
433                });
434            }
435            // reserved(4) = 0, length_outputs(12).
436            buf[pos] = ((length_outputs >> 8) & 0x0F) as u8;
437            buf[pos + 1] = (length_outputs & 0xFF) as u8;
438            pos += 2;
439            for o in &p.outputs {
440                if o.out_signal_desc.len() > u8::MAX as usize {
441                    return Err(Error::InvalidObject {
442                        what: "SelectionInformation",
443                        reason: "out_signal_desc longer than 255 bytes",
444                    });
445                }
446                buf[pos] = o.out_port_id;
447                buf[pos + 1] = o.out_signal_desc.len() as u8;
448                pos += 2;
449                buf[pos..pos + o.out_signal_desc.len()].copy_from_slice(o.out_signal_desc);
450                pos += o.out_signal_desc.len();
451            }
452        }
453        Ok(pos)
454    }
455}
456
457/// One input or output port description within a [`PortProfile`] (Table 48).
458#[derive(Debug, Clone, PartialEq, Eq, Default)]
459#[cfg_attr(feature = "serde", derive(serde::Serialize))]
460pub struct PortDescription<'a> {
461    /// `in_port_id` (Table 44).
462    pub in_port_id: u8,
463    /// `in_port_desc` — string describing the input port (DVB SI Annex A coding).
464    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
465    pub in_port_desc: &'a [u8],
466    /// `out_port_id` (Table 46).
467    pub out_port_id: u8,
468    /// `out_signal_desc` — string describing the output port (DVB SI Annex A).
469    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
470    pub out_port_desc: &'a [u8],
471}
472
473/// `Port profile status data` (Table 48) — status item 2: a textual receiver
474/// identity and per-port descriptions.
475#[derive(Debug, Clone, PartialEq, Eq, Default)]
476#[cfg_attr(feature = "serde", derive(serde::Serialize))]
477pub struct PortProfile<'a> {
478    /// `receiver_identification` — manufacturer/model/version string (DVB SI
479    /// Annex A coding).
480    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
481    pub receiver_identification: &'a [u8],
482    /// One entry per `{in,out}` port pair (`N` entries, consuming the remainder).
483    pub ports: Vec<PortDescription<'a>>,
484}
485
486impl<'a> Parse<'a> for PortProfile<'a> {
487    type Error = Error;
488    fn parse(body: &'a [u8]) -> Result<Self> {
489        if body.is_empty() {
490            return Err(Error::BufferTooShort {
491                need: 1,
492                have: 0,
493                what: "PortProfile.receiver_identification_length",
494            });
495        }
496        let id_len = body[0] as usize;
497        if body.len() < 1 + id_len {
498            return Err(Error::BufferTooShort {
499                need: 1 + id_len,
500                have: body.len(),
501                what: "PortProfile.receiver_identification",
502            });
503        }
504        let receiver_identification = &body[1..1 + id_len];
505        let mut rest = &body[1 + id_len..];
506        let mut ports = Vec::new();
507        while !rest.is_empty() {
508            // in_port_id(1) + length_in_port_desc(1).
509            if rest.len() < 2 {
510                return Err(Error::BufferTooShort {
511                    need: 2,
512                    have: rest.len(),
513                    what: "PortProfile.in_port",
514                });
515            }
516            let in_port_id = rest[0];
517            let in_len = rest[1] as usize;
518            let after_in = 2 + in_len;
519            // + out_port_id(1) + length_out_port_desc(1).
520            if rest.len() < after_in + 2 {
521                return Err(Error::BufferTooShort {
522                    need: after_in + 2,
523                    have: rest.len(),
524                    what: "PortProfile.out_port",
525                });
526            }
527            let in_port_desc = &rest[2..after_in];
528            let out_port_id = rest[after_in];
529            let out_len = rest[after_in + 1] as usize;
530            let after_out = after_in + 2 + out_len;
531            if rest.len() < after_out {
532                return Err(Error::BufferTooShort {
533                    need: after_out,
534                    have: rest.len(),
535                    what: "PortProfile.out_port_desc",
536                });
537            }
538            ports.push(PortDescription {
539                in_port_id,
540                in_port_desc,
541                out_port_id,
542                out_port_desc: &rest[after_in + 2..after_out],
543            });
544            rest = &rest[after_out..];
545        }
546        Ok(Self {
547            receiver_identification,
548            ports,
549        })
550    }
551}
552impl Serialize for PortProfile<'_> {
553    type Error = Error;
554    fn serialized_len(&self) -> usize {
555        let mut n = 1 + self.receiver_identification.len();
556        for p in &self.ports {
557            n += 2 + p.in_port_desc.len() + 2 + p.out_port_desc.len();
558        }
559        n
560    }
561    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
562        let total = self.serialized_len();
563        if buf.len() < total {
564            return Err(Error::OutputBufferTooSmall {
565                need: total,
566                have: buf.len(),
567            });
568        }
569        if self.receiver_identification.len() > u8::MAX as usize {
570            return Err(Error::InvalidObject {
571                what: "PortProfile",
572                reason: "receiver_identification longer than 255 bytes",
573            });
574        }
575        let mut pos = 0;
576        buf[pos] = self.receiver_identification.len() as u8;
577        pos += 1;
578        buf[pos..pos + self.receiver_identification.len()]
579            .copy_from_slice(self.receiver_identification);
580        pos += self.receiver_identification.len();
581        for p in &self.ports {
582            if p.in_port_desc.len() > u8::MAX as usize || p.out_port_desc.len() > u8::MAX as usize {
583                return Err(Error::InvalidObject {
584                    what: "PortProfile",
585                    reason: "port description longer than 255 bytes",
586                });
587            }
588            buf[pos] = p.in_port_id;
589            buf[pos + 1] = p.in_port_desc.len() as u8;
590            pos += 2;
591            buf[pos..pos + p.in_port_desc.len()].copy_from_slice(p.in_port_desc);
592            pos += p.in_port_desc.len();
593            buf[pos] = p.out_port_id;
594            buf[pos + 1] = p.out_port_desc.len() as u8;
595            pos += 2;
596            buf[pos..pos + p.out_port_desc.len()].copy_from_slice(p.out_port_desc);
597            pos += p.out_port_desc.len();
598        }
599        Ok(pos)
600    }
601}
602
603/// `Viewed service status data` (Table 49) — status item 3.
604#[derive(Debug, Clone, PartialEq, Eq, Default)]
605#[cfg_attr(feature = "serde", derive(serde::Serialize))]
606pub struct ViewedService {
607    /// `service_id` / program_number currently selected (`0x0000` = not in the TS).
608    pub service_id: u16,
609    /// `component_tag`s of the components currently selected for decoding.
610    pub component_tags: Vec<u8>,
611}
612
613impl<'a> Parse<'a> for ViewedService {
614    type Error = Error;
615    fn parse(body: &'a [u8]) -> Result<Self> {
616        // service_id(2) + number_components(1).
617        if body.len() < 3 {
618            return Err(Error::BufferTooShort {
619                need: 3,
620                have: body.len(),
621                what: "ViewedService",
622            });
623        }
624        let service_id = u16::from_be_bytes([body[0], body[1]]);
625        let n = body[2] as usize;
626        if body.len() < 3 + n {
627            return Err(Error::BufferTooShort {
628                need: 3 + n,
629                have: body.len(),
630                what: "ViewedService.component_tags",
631            });
632        }
633        Ok(Self {
634            service_id,
635            component_tags: body[3..3 + n].to_vec(),
636        })
637    }
638}
639impl Serialize for ViewedService {
640    type Error = Error;
641    fn serialized_len(&self) -> usize {
642        3 + self.component_tags.len()
643    }
644    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
645        let total = self.serialized_len();
646        if buf.len() < total {
647            return Err(Error::OutputBufferTooSmall {
648                need: total,
649                have: buf.len(),
650            });
651        }
652        if self.component_tags.len() > u8::MAX as usize {
653            return Err(Error::InvalidObject {
654                what: "ViewedService",
655                reason: "more than 255 component tags",
656            });
657        }
658        buf[0..2].copy_from_slice(&self.service_id.to_be_bytes());
659        buf[2] = self.component_tags.len() as u8;
660        buf[3..3 + self.component_tags.len()].copy_from_slice(&self.component_tags);
661        Ok(total)
662    }
663}
664
665/// `activation_state` (Table 51) — host power mode.
666#[derive(Debug, Clone, Copy, PartialEq, Eq)]
667#[cfg_attr(feature = "serde", derive(serde::Serialize))]
668#[non_exhaustive]
669pub enum ActivationState {
670    /// `1` — Standby-active.
671    StandbyActive,
672    /// `2` — On.
673    On,
674    /// `0` and `3`-`7` — reserved.
675    Reserved(u8),
676}
677
678impl ActivationState {
679    /// Decode the 3-bit `activation_state` value.
680    #[must_use]
681    pub fn from_u8(v: u8) -> Self {
682        match v {
683            1 => Self::StandbyActive,
684            2 => Self::On,
685            other => Self::Reserved(other),
686        }
687    }
688    /// 3-bit wire value.
689    #[must_use]
690    pub const fn to_u8(self) -> u8 {
691        match self {
692            Self::StandbyActive => 1,
693            Self::On => 2,
694            Self::Reserved(v) => v & 0x07,
695        }
696    }
697    /// Spec token, or `"reserved"`.
698    #[must_use]
699    pub fn name(&self) -> &'static str {
700        match self {
701            Self::StandbyActive => "Standby-active",
702            Self::On => "On",
703            Self::Reserved(_) => "reserved",
704        }
705    }
706}
707broadcast_common::impl_spec_display!(ActivationState, Reserved);
708
709/// `Activation status data` (Table 50) — status item 4: a single packed byte.
710#[derive(Debug, Clone, Copy, PartialEq, Eq)]
711#[cfg_attr(feature = "serde", derive(serde::Serialize))]
712pub struct ActivationStatus {
713    /// `event_activated` — host activated by an event (vs user action).
714    pub event_activated: bool,
715    /// `activation_state` (3 bits). Always present on the wire: it was an
716    /// `Option` that `parse` filled with `Some` every time, so a serialized
717    /// `None` could never round-trip (audit r10-O-8).
718    pub activation_state: ActivationState,
719    /// The 4 reserved bits `[7:4]` of the status byte, preserved so every
720    /// byte value round-trips (Table 50).
721    pub reserved: u8,
722}
723
724impl ActivationStatus {
725    const EVENT_ACTIVATED_BIT: u8 = 0x08;
726    const ACTIVATION_STATE_MASK: u8 = 0x07;
727    /// Reserved bits `[7:4]` sit above the event bit.
728    const RESERVED_SHIFT: u32 = 4;
729    const RESERVED_MASK: u8 = 0x0F;
730}
731
732impl<'a> Parse<'a> for ActivationStatus {
733    type Error = Error;
734    fn parse(body: &'a [u8]) -> Result<Self> {
735        if body.is_empty() {
736            return Err(Error::BufferTooShort {
737                need: 1,
738                have: 0,
739                what: "ActivationStatus",
740            });
741        }
742        Ok(Self {
743            event_activated: body[0] & Self::EVENT_ACTIVATED_BIT != 0,
744            activation_state: ActivationState::from_u8(body[0] & Self::ACTIVATION_STATE_MASK),
745            reserved: body[0] >> Self::RESERVED_SHIFT,
746        })
747    }
748}
749impl Serialize for ActivationStatus {
750    type Error = Error;
751    fn serialized_len(&self) -> usize {
752        1
753    }
754    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
755        if buf.is_empty() {
756            return Err(Error::OutputBufferTooSmall { need: 1, have: 0 });
757        }
758        let mut b = 0u8;
759        if self.event_activated {
760            b |= Self::EVENT_ACTIVATED_BIT;
761        }
762        b |= self.activation_state.to_u8() & Self::ACTIVATION_STATE_MASK;
763        b |= (self.reserved & Self::RESERVED_MASK) << Self::RESERVED_SHIFT;
764        buf[0] = b;
765        Ok(1)
766    }
767}
768
769crate::dispatch::declare_resource_apdus! {
770    /// Resource-scoped dispatch over the Status Query objects (Tables 37-41).
771    #[derive(Debug, Clone, PartialEq, Eq)]
772    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
773    #[non_exhaustive]
774    pub enum StatusQueryApdu<'a> ("status_query") {
775        /// `StatusQueryReq` (`9F 80 00`).
776        StatusQueryReq(StatusQueryReq) = tag::STATUS_QUERY_REQ,
777        /// `TrapReq` (`9F 80 01`).
778        TrapReq(TrapReq) = tag::TRAP_REQ,
779        /// `GetNextItemReq` (`9F 80 02`).
780        GetNextItemReq(GetNextItemReq) = tag::GET_NEXT_ITEM_REQ,
781        /// `GetNextItemAck` (`9F 80 03`).
782        GetNextItemAck(GetNextItemAck) = tag::GET_NEXT_ITEM_ACK,
783        /// `StatusAck` (`9F 80 04`).
784        StatusAck(StatusAck<'a>) = tag::STATUS_ACK,
785    }
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791
792    #[test]
793    fn status_query_req_round_trips_and_bites() {
794        let req = StatusQueryReq {
795            status_item: StatusItem::PortProfile,
796        };
797        let bytes = req.to_bytes();
798        // tag(3) + len(1=4) + StatusItem(4 = 0x00000002).
799        assert_eq!(bytes, [0x9F, 0x80, 0x00, 0x04, 0x00, 0x00, 0x00, 0x02]);
800        assert_eq!(StatusQueryReq::parse(&bytes).unwrap(), req);
801        let other = StatusQueryReq {
802            status_item: StatusItem::ViewedService,
803        };
804        assert_ne!(bytes, other.to_bytes());
805    }
806
807    #[test]
808    fn trap_and_getnext_round_trip() {
809        let trap = TrapReq {
810            status_item: StatusItem::ActivationStatus,
811        };
812        assert_eq!(trap.to_bytes(), [0x9F, 0x80, 0x01, 0x04, 0, 0, 0, 4]);
813        assert_eq!(TrapReq::parse(&trap.to_bytes()).unwrap(), trap);
814
815        let gnr = GetNextItemReq {
816            start_status_item: 0x0000_0007,
817        };
818        assert_eq!(gnr.to_bytes(), [0x9F, 0x80, 0x02, 0x04, 0, 0, 0, 7]);
819        assert_eq!(GetNextItemReq::parse(&gnr.to_bytes()).unwrap(), gnr);
820
821        let gna = GetNextItemAck {
822            next_status_item: 0x0000_0002,
823        };
824        assert_eq!(gna.to_bytes(), [0x9F, 0x80, 0x03, 0x04, 0, 0, 0, 2]);
825        assert_eq!(GetNextItemAck::parse(&gna.to_bytes()).unwrap(), gna);
826    }
827
828    #[test]
829    fn status_item_reserved_round_trips() {
830        let req = StatusQueryReq {
831            status_item: StatusItem::from_u32(0),
832        };
833        assert_eq!(req.status_item, StatusItem::Reserved(0));
834        assert_eq!(req.status_item.name(), "reserved");
835        assert_eq!(StatusQueryReq::parse(&req.to_bytes()).unwrap(), req);
836    }
837
838    #[test]
839    fn status_ack_with_status_bytes_round_trips_and_bites() {
840        let ack = StatusAck {
841            status_item: StatusItem::ViewedService,
842            status_bytes: &[0x01, 0x23, 0x00],
843        };
844        let bytes = ack.to_bytes();
845        // tag(3) + len(1=7) + item(4) + 3 status bytes.
846        assert_eq!(
847            bytes,
848            [0x9F, 0x80, 0x04, 0x07, 0, 0, 0, 3, 0x01, 0x23, 0x00]
849        );
850        assert_eq!(StatusAck::parse(&bytes).unwrap(), ack);
851        let mut other = ack.clone();
852        other.status_bytes = &[0x01, 0x23, 0x01];
853        assert_ne!(bytes, other.to_bytes());
854    }
855
856    #[test]
857    fn status_ack_empty_status_bytes_means_unsupported() {
858        let ack = StatusAck {
859            status_item: StatusItem::PortProfile,
860            status_bytes: &[],
861        };
862        let bytes = ack.to_bytes();
863        assert_eq!(bytes, [0x9F, 0x80, 0x04, 0x04, 0, 0, 0, 2]);
864        assert_eq!(StatusAck::parse(&bytes).unwrap(), ack);
865    }
866
867    #[test]
868    fn selection_information_multi_port_round_trips_and_bites() {
869        // Two input ports, the first routed to two outputs (boundary >= 2 loop).
870        let si = SelectionInformation {
871            time: &[0x20, 0x06, 0x18, 0x12, 0x00],
872            ports: alloc::vec![
873                InPortDescription {
874                    in_port_id: 0x01,
875                    in_signal_desc: &[0xAA, 0xBB],
876                    outputs: alloc::vec![
877                        OutputSignal {
878                            out_port_id: 0x00,
879                            out_signal_desc: &[0x02],
880                        },
881                        OutputSignal {
882                            out_port_id: 0x01,
883                            out_signal_desc: &[0x01],
884                        },
885                    ],
886                },
887                InPortDescription {
888                    in_port_id: 0x18,
889                    in_signal_desc: &[],
890                    outputs: alloc::vec![],
891                },
892            ],
893        };
894        let bytes = si.to_bytes();
895        assert_eq!(SelectionInformation::parse(&bytes).unwrap(), si);
896        // Mutate one out_port_id -> different bytes.
897        let mut other = si.clone();
898        other.ports[0].outputs[1].out_port_id = 0x02;
899        assert_ne!(bytes, other.to_bytes());
900        // Verify a precise byte layout for the first port's output block.
901        // time(5) | in_port(0x01) in_len(0x02) [AA BB] | reserved/len_out(0x00 0x06)
902        //   out0: 00 01 02 | out1: 01 01 01 | in_port(0x18) in_len(0x00) len_out(0x00 0x00)
903        assert_eq!(
904            bytes,
905            [
906                0x20, 0x06, 0x18, 0x12, 0x00, // time
907                0x01, 0x02, 0xAA, 0xBB, // in port 1
908                0x00, 0x06, // length_outputs = 6
909                0x00, 0x01, 0x02, // out 0
910                0x01, 0x01, 0x01, // out 1
911                0x18, 0x00, // in port 2
912                0x00, 0x00, // length_outputs = 0
913            ]
914        );
915    }
916
917    #[test]
918    fn port_profile_multi_port_round_trips_and_bites() {
919        let pp = PortProfile {
920            receiver_identification: b"ACME-TV1",
921            ports: alloc::vec![
922                PortDescription {
923                    in_port_id: 0x00,
924                    in_port_desc: b"RF0",
925                    out_port_id: 0x00,
926                    out_port_desc: b"D0",
927                },
928                PortDescription {
929                    in_port_id: 0x18,
930                    in_port_desc: b"CI",
931                    out_port_id: 0x10,
932                    out_port_desc: b"SCART",
933                },
934            ],
935        };
936        let bytes = pp.to_bytes();
937        assert_eq!(PortProfile::parse(&bytes).unwrap(), pp);
938        let mut other = pp.clone();
939        other.ports[1].out_port_id = 0x11;
940        assert_ne!(bytes, other.to_bytes());
941    }
942
943    #[test]
944    fn viewed_service_multi_component_round_trips_and_bites() {
945        let vs = ViewedService {
946            service_id: 0x1234,
947            component_tags: alloc::vec![0x10, 0x20, 0x30],
948        };
949        let bytes = vs.to_bytes();
950        assert_eq!(bytes, [0x12, 0x34, 0x03, 0x10, 0x20, 0x30]);
951        assert_eq!(ViewedService::parse(&bytes).unwrap(), vs);
952        let mut other = vs.clone();
953        other.component_tags[2] = 0x31;
954        assert_ne!(bytes, other.to_bytes());
955    }
956
957    #[test]
958    fn activation_status_packs_bits() {
959        let a = ActivationStatus {
960            event_activated: true,
961            activation_state: ActivationState::On,
962            reserved: 0,
963        };
964        let bytes = a.to_bytes();
965        // event_activated(1<<3) | activation_state(2) = 0x0A.
966        assert_eq!(bytes, [0x0A]);
967        assert_eq!(ActivationStatus::parse(&bytes).unwrap(), a);
968        // Standby-active, no event.
969        let b = ActivationStatus {
970            event_activated: false,
971            activation_state: ActivationState::StandbyActive,
972            reserved: 0,
973        };
974        assert_eq!(b.to_bytes(), [0x01]);
975        assert_eq!(ActivationStatus::parse(&[0x01]).unwrap(), b);
976        // Every one of the 256 byte values (reserved states and reserved bits included) now
977        // round-trips: the old `Option` state could not.
978        for byte in 0x00u8..=0xFF {
979            let parsed = ActivationStatus::parse(&[byte]).unwrap();
980            assert_eq!(parsed.to_bytes(), [byte], "byte {byte:#04X}");
981        }
982    }
983
984    #[test]
985    fn dispatch_routes_each_tag() {
986        let q = StatusQueryReq {
987            status_item: StatusItem::SelectionInformation,
988        }
989        .to_bytes();
990        assert!(matches!(
991            StatusQueryApdu::parse(&q).unwrap(),
992            StatusQueryApdu::StatusQueryReq(_)
993        ));
994        let ack = StatusAck {
995            status_item: StatusItem::ActivationStatus,
996            status_bytes: &[0x0A],
997        }
998        .to_bytes();
999        let parsed = StatusQueryApdu::parse(&ack).unwrap();
1000        assert!(matches!(parsed, StatusQueryApdu::StatusAck(_)));
1001        assert_eq!(parsed.to_bytes(), ack);
1002    }
1003}