dvb-ci 0.12.0

DVB Common Interface (EN 50221) — APDU/resource objects, session/transport PDUs, and a CA_PMT builder from a dvb-si PMT.
Documentation
//! Power Manager objects — ETSI TS 101 699 V1.1.1 §6.3, Tables 52-55
//! (PDF pp. 51-52). See `private/specs-verified/dvb/dvb-ci/ci_plus/power-manager.md`.
//!
//! Resource ID `0x00220041`. Lets a module tell the host it is busy with a task
//! that should complete before the host powers down.
//!
//! - `activation_state_change_request` (`9F 80 00`, Table 52) — host asks the
//!   module to change activation state.
//! - `activation_state_change_ack` (`9F 80 01`, Table 54) — module's reply.

use crate::error::{Error, Result};
use crate::objects;
use crate::reserved::ReservedZero;
use broadcast_common::len::need_in;
use broadcast_common::{Parse, Serialize};

/// Resource-scoped `apdu_tag`s for the Power Manager (Tables 52, 54).
pub mod tag {
    use crate::tag::ApduTag;
    /// `activation_status_change_request_tag` = `9F 80 00`.
    pub const ACTIVATION_STATE_CHANGE_REQUEST: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x00);
    /// `activation_status_change_ack_tag` = `9F 80 01`.
    pub const ACTIVATION_STATE_CHANGE_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x01);
}

/// `activation_state` — the requested power mode (Table 53).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum ActivationState {
    /// `0` — Standby-passive (the EACEM-defined power mode).
    StandbyPassive,
    /// `1`-`15` — reserved for future use.
    Reserved(u8),
}

impl ActivationState {
    /// Decode the 4-bit `activation_state`.
    #[must_use]
    pub fn from_u8(v: u8) -> Self {
        match v & 0x0F {
            0 => Self::StandbyPassive,
            other => Self::Reserved(other),
        }
    }
    /// 4-bit wire value.
    #[must_use]
    pub const fn to_u8(self) -> u8 {
        match self {
            Self::StandbyPassive => 0,
            Self::Reserved(v) => v & 0x0F,
        }
    }
    /// Spec token, or `"reserved"`.
    #[must_use]
    pub fn name(&self) -> &'static str {
        match self {
            Self::StandbyPassive => "Standby-passive",
            Self::Reserved(_) => "reserved",
        }
    }
}
broadcast_common::impl_spec_display!(ActivationState, Reserved);

/// `reply_code` — the module's response to a state-change request (Table 55).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum ReplyCode {
    /// `0` — OK to change state.
    Ok,
    /// `1` — Module busy, don't change state.
    Busy,
    /// `2`-`255` — reserved for future use.
    Reserved(u8),
}

impl ReplyCode {
    /// Decode the `reply_code` byte.
    #[must_use]
    pub fn from_u8(v: u8) -> Self {
        match v {
            0 => Self::Ok,
            1 => Self::Busy,
            other => Self::Reserved(other),
        }
    }
    /// Wire byte.
    #[must_use]
    pub const fn to_u8(self) -> u8 {
        match self {
            Self::Ok => 0,
            Self::Busy => 1,
            Self::Reserved(v) => v,
        }
    }
    /// Spec token, or `"reserved"`.
    #[must_use]
    pub fn name(&self) -> &'static str {
        match self {
            Self::Ok => "OK to change state",
            Self::Busy => "Module busy, don't change state",
            Self::Reserved(_) => "reserved",
        }
    }
}
broadcast_common::impl_spec_display!(ReplyCode, Reserved);

/// `activation_state_change_request()` (Table 52): host → module.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ActivationStateChangeRequest {
    /// How the `length_field` was coded on the wire: a non-minimal long form
    /// (e.g. `82 00 09`) is preserved so the object re-serializes
    /// byte-for-byte; [`LengthForm::Minimal`](crate::length::LengthForm::Minimal) for a hand-built value.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub length_form: crate::length::LengthForm,

    /// The requested new activation state (low 4 bits; top 4 bits reserved).
    pub activation_state: ActivationState,
    /// The 4-bit `reserved` field in front of `activation_state` as received, preserved so the
    /// object re-serializes byte-for-byte; `0` when hand-built.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub reserved: ReservedZero<4>,
}

/// `activation_state_change_ack()` (Table 54): module → host.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ActivationStateChangeAck {
    /// How the `length_field` was coded on the wire: a non-minimal long form
    /// (e.g. `82 00 09`) is preserved so the object re-serializes
    /// byte-for-byte; [`LengthForm::Minimal`](crate::length::LengthForm::Minimal) for a hand-built value.
    #[cfg_attr(feature = "serde", serde(skip))]
    pub length_form: crate::length::LengthForm,

    /// The module's response.
    pub reply_code: ReplyCode,
}

// reserved(4) + activation_state(4).
const REQUEST_BODY: usize = 1;
// reply_code(8).
const ACK_BODY: usize = 1;

impl<'a> Parse<'a> for ActivationStateChangeRequest {
    type Error = Error;
    fn parse(bytes: &'a [u8]) -> Result<Self> {
        let (length_form, body) = objects::parse_apdu_header(
            bytes,
            tag::ACTIVATION_STATE_CHANGE_REQUEST,
            "activation_state_change_request",
        )?;
        need_in(body, REQUEST_BODY, "activation_state_change_request")?;
        crate::objects::reject_trailing_body(
            body,
            REQUEST_BODY,
            "activation_state_change_request",
        )?;
        Ok(Self {
            length_form,
            activation_state: ActivationState::from_u8(body[0] & 0x0F),
            reserved: ReservedZero::from_bits(body[0] >> 4),
        })
    }
}
impl Serialize for ActivationStateChangeRequest {
    type Error = Error;
    fn serialized_len(&self) -> usize {
        objects::apdu_len(REQUEST_BODY, self.length_form)
    }
    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
        let pos = objects::write_apdu_header(
            tag::ACTIVATION_STATE_CHANGE_REQUEST,
            REQUEST_BODY,
            self.length_form,
            buf,
        )?;
        // reserved(4), activation_state(4).
        buf[pos] = (self.reserved.get() << 4) | (self.activation_state.to_u8() & 0x0F);
        Ok(pos + REQUEST_BODY)
    }
}

impl<'a> Parse<'a> for ActivationStateChangeAck {
    type Error = Error;
    fn parse(bytes: &'a [u8]) -> Result<Self> {
        let (length_form, body) = objects::parse_apdu_header(
            bytes,
            tag::ACTIVATION_STATE_CHANGE_ACK,
            "activation_state_change_ack",
        )?;
        need_in(body, ACK_BODY, "activation_state_change_ack")?;
        crate::objects::reject_trailing_body(body, ACK_BODY, "activation_state_change_ack")?;
        Ok(Self {
            length_form,
            reply_code: ReplyCode::from_u8(body[0]),
        })
    }
}
impl Serialize for ActivationStateChangeAck {
    type Error = Error;
    fn serialized_len(&self) -> usize {
        objects::apdu_len(ACK_BODY, self.length_form)
    }
    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
        let pos = objects::write_apdu_header(
            tag::ACTIVATION_STATE_CHANGE_ACK,
            ACK_BODY,
            self.length_form,
            buf,
        )?;
        buf[pos] = self.reply_code.to_u8();
        Ok(pos + ACK_BODY)
    }
}

crate::dispatch::declare_resource_apdus! {
    /// Resource-scoped dispatch over the Power Manager objects.
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
    #[non_exhaustive]
    pub enum PowerManagerApdu ("power_manager") {
        /// `activation_state_change_request` (`9F 80 00`).
        Request(ActivationStateChangeRequest) = tag::ACTIVATION_STATE_CHANGE_REQUEST,
        /// `activation_state_change_ack` (`9F 80 01`).
        Ack(ActivationStateChangeAck) = tag::ACTIVATION_STATE_CHANGE_ACK,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn request_round_trips_and_bites() {
        let r = ActivationStateChangeRequest {
            reserved: Default::default(),
            length_form: crate::length::LengthForm::Minimal,
            activation_state: ActivationState::StandbyPassive,
        };
        let bytes = r.try_to_bytes().unwrap();
        assert_eq!(bytes, [0x9F, 0x80, 0x00, 0x01, 0x00]);
        assert_eq!(ActivationStateChangeRequest::parse(&bytes).unwrap(), r);
        assert_eq!(r.activation_state.name(), "Standby-passive");
        let other = ActivationStateChangeRequest {
            reserved: Default::default(),
            length_form: crate::length::LengthForm::Minimal,
            activation_state: ActivationState::Reserved(5),
        };
        assert_ne!(bytes, other.try_to_bytes().unwrap());
        assert_eq!(other.try_to_bytes().unwrap()[4], 0x05);
    }

    #[test]
    fn ack_round_trips_and_bites() {
        let a = ActivationStateChangeAck {
            length_form: crate::length::LengthForm::Minimal,
            reply_code: ReplyCode::Busy,
        };
        let bytes = a.try_to_bytes().unwrap();
        assert_eq!(bytes, [0x9F, 0x80, 0x01, 0x01, 0x01]);
        assert_eq!(ActivationStateChangeAck::parse(&bytes).unwrap(), a);
        assert_eq!(a.reply_code.name(), "Module busy, don't change state");
        let other = ActivationStateChangeAck {
            length_form: crate::length::LengthForm::Minimal,
            reply_code: ReplyCode::Ok,
        };
        assert_ne!(bytes, other.try_to_bytes().unwrap());
    }

    /// `declare_resource_apdus!` surface: the declared tag list, and the
    /// error a foreign tag produces (names the first declared tag).
    #[test]
    fn declared_dispatch_lists_tags_and_rejects_foreign_ones() {
        let tags: alloc::vec::Vec<u32> =
            PowerManagerApdu::TAGS.iter().map(|t| t.as_u24()).collect();
        assert_eq!(tags, [0x9F_8000, 0x9F_8001]);
        // 9F8002 is not a Power Manager object.
        match PowerManagerApdu::parse(&[0x9F, 0x80, 0x02, 0x00]) {
            Err(Error::UnexpectedApduTag {
                got,
                expected,
                what,
            }) => {
                assert_eq!(got, 0x9F_8002);
                assert_eq!(expected, 0x9F_8000);
                assert_eq!(what, "power_manager");
            }
            other => panic!("expected UnexpectedApduTag, got {other:?}"),
        }
        assert!(matches!(
            PowerManagerApdu::parse(&[0x9F, 0x80]),
            Err(Error::BufferTooShort(
                broadcast_common::error::BufferTooShort {
                    need: 3,
                    have: 2,
                    ..
                }
            ))
        ));
    }

    #[test]
    fn dispatch_routes_both_tags() {
        let req = ActivationStateChangeRequest {
            reserved: Default::default(),
            length_form: crate::length::LengthForm::Minimal,
            activation_state: ActivationState::StandbyPassive,
        }
        .try_to_bytes()
        .unwrap();
        assert!(matches!(
            PowerManagerApdu::parse(&req).unwrap(),
            PowerManagerApdu::Request(_)
        ));
        let ack = ActivationStateChangeAck {
            length_form: crate::length::LengthForm::Minimal,
            reply_code: ReplyCode::Ok,
        }
        .try_to_bytes()
        .unwrap();
        let parsed = PowerManagerApdu::parse(&ack).unwrap();
        assert!(matches!(parsed, PowerManagerApdu::Ack(_)));
        assert_eq!(parsed.try_to_bytes().unwrap(), ack);
    }
}