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