Skip to main content

dvb_ci/objects/
resource_manager.rs

1//! Resource Manager objects — ETSI EN 50221 §8.4.1, Tables 17-19 (PDF pp. 26-27).
2//!
3//! - `profile_enq` (`9F 80 10`, Table 17) — header-only enquiry.
4//! - `profile` reply (`9F 80 11`, Table 18) — list of `resource_identifier()`s.
5//! - `profile_change` (`9F 80 12`, Table 19) — header-only notification.
6
7use crate::error::{Error, Result};
8use crate::resource::ResourceId;
9use crate::tag::{self, ApduTag};
10use crate::traits::ApduDef;
11use alloc::vec::Vec;
12use broadcast_common::{Parse, Serialize};
13
14/// `profile_enq()` — Profile Enquiry, an empty-body object (Table 17).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize))]
17pub struct ProfileEnq;
18
19/// `profile_changed()` — Profile Changed, an empty-body object (Table 19).
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize))]
22pub struct ProfileChange;
23
24/// `profile()` reply — the list of resources the sender provides (Table 18).
25#[derive(Debug, Clone, PartialEq, Eq, Default)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize))]
27pub struct Profile {
28    /// The advertised `resource_identifier()`s, in wire order.
29    pub resources: Vec<ResourceId>,
30}
31
32// --- profile_enq (empty body) ---
33
34impl<'a> Parse<'a> for ProfileEnq {
35    type Error = Error;
36    fn parse(bytes: &'a [u8]) -> Result<Self> {
37        super::parse_empty_apdu(bytes, tag::PROFILE_ENQ, "profile_enq")?;
38        Ok(Self)
39    }
40}
41impl Serialize for ProfileEnq {
42    type Error = Error;
43    fn serialized_len(&self) -> usize {
44        super::empty_apdu_len()
45    }
46    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
47        super::serialize_empty_apdu(tag::PROFILE_ENQ, buf)
48    }
49}
50impl ApduDef<'_> for ProfileEnq {
51    const TAG: ApduTag = tag::PROFILE_ENQ;
52    const NAME: &'static str = "PROFILE_ENQ";
53}
54
55// --- profile_change (empty body) ---
56
57impl<'a> Parse<'a> for ProfileChange {
58    type Error = Error;
59    fn parse(bytes: &'a [u8]) -> Result<Self> {
60        super::parse_empty_apdu(bytes, tag::PROFILE_CHANGE, "profile_change")?;
61        Ok(Self)
62    }
63}
64impl Serialize for ProfileChange {
65    type Error = Error;
66    fn serialized_len(&self) -> usize {
67        super::empty_apdu_len()
68    }
69    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
70        super::serialize_empty_apdu(tag::PROFILE_CHANGE, buf)
71    }
72}
73impl ApduDef<'_> for ProfileChange {
74    const TAG: ApduTag = tag::PROFILE_CHANGE;
75    const NAME: &'static str = "PROFILE_CHANGE";
76}
77
78// --- profile (reply, resource-id list) ---
79
80impl<'a> Parse<'a> for Profile {
81    type Error = Error;
82    fn parse(bytes: &'a [u8]) -> Result<Self> {
83        let body = super::parse_apdu_header(bytes, tag::PROFILE, "profile")?;
84        if body.len() % ResourceId::LEN != 0 {
85            return Err(Error::InvalidObject {
86                what: "profile",
87                reason: "body length is not a multiple of 4",
88            });
89        }
90        let mut resources = Vec::with_capacity(body.len() / ResourceId::LEN);
91        for chunk in body.chunks_exact(ResourceId::LEN) {
92            resources.push(ResourceId::parse(chunk)?);
93        }
94        Ok(Self { resources })
95    }
96}
97
98impl Serialize for Profile {
99    type Error = Error;
100    fn serialized_len(&self) -> usize {
101        let body = self.resources.len() * ResourceId::LEN;
102        super::apdu_len(body)
103    }
104    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
105        let body_len = self.resources.len() * ResourceId::LEN;
106        let mut pos = super::write_apdu_header(tag::PROFILE, body_len, buf)?;
107        for r in &self.resources {
108            pos += r.serialize_into(&mut buf[pos..])?;
109        }
110        Ok(pos)
111    }
112}
113
114impl ApduDef<'_> for Profile {
115    const TAG: ApduTag = tag::PROFILE;
116    const NAME: &'static str = "PROFILE";
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::resource::{APPLICATION_INFORMATION, CONDITIONAL_ACCESS_SUPPORT, RESOURCE_MANAGER};
123
124    #[test]
125    fn profile_enq_round_trip() {
126        let bytes = ProfileEnq.to_bytes();
127        assert_eq!(bytes, [0x9F, 0x80, 0x10, 0x00]);
128        assert_eq!(ProfileEnq::parse(&bytes).unwrap(), ProfileEnq);
129    }
130
131    #[test]
132    fn profile_change_round_trip() {
133        let bytes = ProfileChange.to_bytes();
134        assert_eq!(bytes, [0x9F, 0x80, 0x12, 0x00]);
135        assert_eq!(ProfileChange::parse(&bytes).unwrap(), ProfileChange);
136    }
137
138    #[test]
139    fn profile_reply_multi_resource_round_trips() {
140        let p = Profile {
141            resources: alloc::vec![
142                RESOURCE_MANAGER,
143                APPLICATION_INFORMATION,
144                CONDITIONAL_ACCESS_SUPPORT,
145            ],
146        };
147        let bytes = p.to_bytes();
148        // header(3) + length(1) + 3*4 = 16
149        assert_eq!(bytes.len(), 16);
150        assert_eq!(&bytes[..4], &[0x9F, 0x80, 0x11, 0x0C]); // length 12
151        let parsed = Profile::parse(&bytes).unwrap();
152        assert_eq!(parsed, p);
153        assert_eq!(parsed.resources.len(), 3);
154    }
155
156    #[test]
157    fn mutating_resource_changes_bytes() {
158        let p = Profile {
159            resources: alloc::vec![RESOURCE_MANAGER],
160        };
161        let bytes = p.to_bytes();
162        let mut other = p.clone();
163        other.resources[0] = MMI_RES;
164        assert_ne!(bytes, other.to_bytes());
165    }
166
167    const MMI_RES: ResourceId = crate::resource::MMI;
168}