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