Skip to main content

dvb_ci/ci_ext/
resource_manager_v2.rs

1//! Resource Manager v2 objects — ETSI TS 101 699 V1.1.1 §4.2.1, Tables 3-7
2//! (PDF pp. 13-17). See `docs/ci_plus/resource-manager-v2.md`.
3//!
4//! Resource ID `0x00010042`. Adds Module ID establishment to the EN 50221 v1
5//! Resource Manager. The three v1 objects (Profile Enquiry, Profile Reply,
6//! Profile Changed) are layout-identical to EN 50221 but are re-defined here so
7//! the v2 resource owns its own object set:
8//!
9//! - `profile_enq` (`9F 80 10`, Table 3) — header-only enquiry.
10//! - `profile_reply` (`9F 80 11`, Table 4) — list of `resource_identifier()`s.
11//! - `profile_changed` (`9F 80 12`, Table 5) — header-only notification.
12//! - `module_id_send` (`9F 80 13`, Table 6) — module returns its Module ID.
13//! - `module_id_command` (`9F 80 14`, Table 7) — host ack / sets the Module ID.
14
15use crate::error::{Error, Result};
16use crate::objects;
17use crate::resource::ResourceId;
18use crate::tag::ApduTag;
19use alloc::vec::Vec;
20use broadcast_common::{Parse, Serialize};
21
22/// Resource-scoped `apdu_tag`s for the Resource Manager v2 (Table 87 / Tables 3-7).
23pub mod tag {
24    use crate::tag::ApduTag;
25    /// `Tprofile_enq` = `9F 80 10`.
26    pub const PROFILE_ENQ: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x10);
27    /// `Tprofile_reply` = `9F 80 11`.
28    pub const PROFILE_REPLY: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x11);
29    /// `Tprofile_changed` = `9F 80 12`.
30    pub const PROFILE_CHANGED: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x12);
31    /// `Tmodule_id_send` = `9F 80 13`.
32    pub const MODULE_ID_SEND: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x13);
33    /// `Tmodule_id_command` = `9F 80 14`.
34    pub const MODULE_ID_COMMAND: ApduTag = ApduTag::from_bytes(0x9F, 0x80, 0x14);
35}
36
37/// `profile_enq()` — Profile Enquiry, empty body (Table 3).
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize))]
40pub struct ProfileEnq;
41
42/// `profile_changed()` — Profile Changed, empty body (Table 5).
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize))]
45pub struct ProfileChanged;
46
47/// `profile_reply()` — the list of resources the sender provides (Table 4).
48#[derive(Debug, Clone, PartialEq, Eq, Default)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize))]
50pub struct ProfileReply {
51    /// Advertised `resource_identifier()`s, in wire order (`length_field = N*4`).
52    pub resources: Vec<ResourceId>,
53}
54
55/// `module_id_send()` — module returns its current Module ID (Table 6).
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize))]
58pub struct ModuleIdSend {
59    /// The 6-bit Module ID (`0` if the host has not allocated one). Only the low
60    /// 6 bits are significant; the top 2 bits are reserved.
61    pub module_id: u8,
62}
63
64/// `command` values for `module_id_command()` (Table 7).
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize))]
67#[non_exhaustive]
68pub enum ModuleIdCommandKind {
69    /// `0x01` — host accepts the Module ID; module continues to Profile.
70    Acknowledgement,
71    /// `0x02` — `module_id` carries a new ID to set.
72    SetModuleId,
73    /// Any other value (reserved).
74    Reserved(u8),
75}
76
77impl ModuleIdCommandKind {
78    /// Decode a `command` byte.
79    #[must_use]
80    pub fn from_u8(v: u8) -> Self {
81        match v {
82            0x01 => Self::Acknowledgement,
83            0x02 => Self::SetModuleId,
84            other => Self::Reserved(other),
85        }
86    }
87    /// Wire byte.
88    #[must_use]
89    pub const fn to_u8(self) -> u8 {
90        match self {
91            Self::Acknowledgement => 0x01,
92            Self::SetModuleId => 0x02,
93            Self::Reserved(v) => v,
94        }
95    }
96    /// Spec token, or `"reserved"`.
97    #[must_use]
98    pub fn name(&self) -> &'static str {
99        match self {
100            Self::Acknowledgement => "Acknowledgement",
101            Self::SetModuleId => "Set_ModuleID",
102            Self::Reserved(_) => "reserved",
103        }
104    }
105}
106broadcast_common::impl_spec_display!(ModuleIdCommandKind, Reserved);
107
108/// `module_id_command()` — host acknowledges or sets the Module ID (Table 7).
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize))]
111pub struct ModuleIdCommand {
112    /// `command`.
113    pub command: ModuleIdCommandKind,
114    /// The 6-bit Module ID (significant only for `Set_ModuleID`).
115    pub module_id: u8,
116}
117
118// --- profile_enq / profile_changed (empty body) ---
119
120impl<'a> Parse<'a> for ProfileEnq {
121    type Error = Error;
122    fn parse(bytes: &'a [u8]) -> Result<Self> {
123        objects::parse_empty_apdu(bytes, tag::PROFILE_ENQ, "profile_enq")?;
124        Ok(Self)
125    }
126}
127impl Serialize for ProfileEnq {
128    type Error = Error;
129    fn serialized_len(&self) -> usize {
130        objects::empty_apdu_len()
131    }
132    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
133        objects::serialize_empty_apdu(tag::PROFILE_ENQ, buf)
134    }
135}
136
137impl<'a> Parse<'a> for ProfileChanged {
138    type Error = Error;
139    fn parse(bytes: &'a [u8]) -> Result<Self> {
140        objects::parse_empty_apdu(bytes, tag::PROFILE_CHANGED, "profile_changed")?;
141        Ok(Self)
142    }
143}
144impl Serialize for ProfileChanged {
145    type Error = Error;
146    fn serialized_len(&self) -> usize {
147        objects::empty_apdu_len()
148    }
149    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
150        objects::serialize_empty_apdu(tag::PROFILE_CHANGED, buf)
151    }
152}
153
154// --- profile_reply (resource-id list) ---
155
156impl<'a> Parse<'a> for ProfileReply {
157    type Error = Error;
158    fn parse(bytes: &'a [u8]) -> Result<Self> {
159        let body = objects::parse_apdu_header(bytes, tag::PROFILE_REPLY, "profile_reply")?;
160        if body.len() % ResourceId::LEN != 0 {
161            return Err(Error::InvalidObject {
162                what: "profile_reply",
163                reason: "body length is not a multiple of 4",
164            });
165        }
166        let mut resources = Vec::with_capacity(body.len() / ResourceId::LEN);
167        for chunk in body.chunks_exact(ResourceId::LEN) {
168            resources.push(ResourceId::parse(chunk)?);
169        }
170        Ok(Self { resources })
171    }
172}
173impl Serialize for ProfileReply {
174    type Error = Error;
175    fn serialized_len(&self) -> usize {
176        objects::apdu_len(self.resources.len() * ResourceId::LEN)
177    }
178    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
179        let body_len = self.resources.len() * ResourceId::LEN;
180        let mut pos = objects::write_apdu_header(tag::PROFILE_REPLY, body_len, buf)?;
181        for r in &self.resources {
182            pos += r.serialize_into(&mut buf[pos..])?;
183        }
184        Ok(pos)
185    }
186}
187
188// --- module_id_send ---
189
190// reserved(2) + module_id(6).
191const MODULE_ID_SEND_BODY: usize = 1;
192
193impl<'a> Parse<'a> for ModuleIdSend {
194    type Error = Error;
195    fn parse(bytes: &'a [u8]) -> Result<Self> {
196        let body = objects::parse_apdu_header(bytes, tag::MODULE_ID_SEND, "module_id_send")?;
197        if body.len() < MODULE_ID_SEND_BODY {
198            return Err(Error::BufferTooShort {
199                need: MODULE_ID_SEND_BODY,
200                have: body.len(),
201                what: "module_id_send",
202            });
203        }
204        Ok(Self {
205            module_id: body[0] & 0x3F,
206        })
207    }
208}
209impl Serialize for ModuleIdSend {
210    type Error = Error;
211    fn serialized_len(&self) -> usize {
212        objects::apdu_len(MODULE_ID_SEND_BODY)
213    }
214    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
215        let pos = objects::write_apdu_header(tag::MODULE_ID_SEND, MODULE_ID_SEND_BODY, buf)?;
216        // reserved(2)='00', module_id(6).
217        buf[pos] = self.module_id & 0x3F;
218        Ok(pos + MODULE_ID_SEND_BODY)
219    }
220}
221
222// --- module_id_command ---
223
224// command(8) + reserved(2) + module_id(6).
225const MODULE_ID_COMMAND_BODY: usize = 2;
226
227impl<'a> Parse<'a> for ModuleIdCommand {
228    type Error = Error;
229    fn parse(bytes: &'a [u8]) -> Result<Self> {
230        let body = objects::parse_apdu_header(bytes, tag::MODULE_ID_COMMAND, "module_id_command")?;
231        if body.len() < MODULE_ID_COMMAND_BODY {
232            return Err(Error::BufferTooShort {
233                need: MODULE_ID_COMMAND_BODY,
234                have: body.len(),
235                what: "module_id_command",
236            });
237        }
238        Ok(Self {
239            command: ModuleIdCommandKind::from_u8(body[0]),
240            module_id: body[1] & 0x3F,
241        })
242    }
243}
244impl Serialize for ModuleIdCommand {
245    type Error = Error;
246    fn serialized_len(&self) -> usize {
247        objects::apdu_len(MODULE_ID_COMMAND_BODY)
248    }
249    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
250        let pos = objects::write_apdu_header(tag::MODULE_ID_COMMAND, MODULE_ID_COMMAND_BODY, buf)?;
251        buf[pos] = self.command.to_u8();
252        buf[pos + 1] = self.module_id & 0x3F;
253        Ok(pos + MODULE_ID_COMMAND_BODY)
254    }
255}
256
257/// Resource-scoped dispatch over the Resource Manager v2 objects (Tables 3-7).
258#[derive(Debug, Clone, PartialEq, Eq)]
259#[cfg_attr(feature = "serde", derive(serde::Serialize))]
260#[non_exhaustive]
261pub enum ResourceManagerV2Apdu {
262    /// `profile_enq` (`9F 80 10`).
263    ProfileEnq(ProfileEnq),
264    /// `profile_reply` (`9F 80 11`).
265    ProfileReply(ProfileReply),
266    /// `profile_changed` (`9F 80 12`).
267    ProfileChanged(ProfileChanged),
268    /// `module_id_send` (`9F 80 13`).
269    ModuleIdSend(ModuleIdSend),
270    /// `module_id_command` (`9F 80 14`).
271    ModuleIdCommand(ModuleIdCommand),
272}
273
274impl ResourceManagerV2Apdu {
275    /// Parse a Resource Manager v2 APDU, dispatching on the leading `apdu_tag`.
276    pub fn parse(body: &[u8]) -> Result<Self> {
277        if body.len() < 3 {
278            return Err(Error::BufferTooShort {
279                need: 3,
280                have: body.len(),
281                what: "resource_manager_v2 apdu_tag",
282            });
283        }
284        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
285        match t {
286            tag::PROFILE_ENQ => Ok(Self::ProfileEnq(ProfileEnq::parse(body)?)),
287            tag::PROFILE_REPLY => Ok(Self::ProfileReply(ProfileReply::parse(body)?)),
288            tag::PROFILE_CHANGED => Ok(Self::ProfileChanged(ProfileChanged::parse(body)?)),
289            tag::MODULE_ID_SEND => Ok(Self::ModuleIdSend(ModuleIdSend::parse(body)?)),
290            tag::MODULE_ID_COMMAND => Ok(Self::ModuleIdCommand(ModuleIdCommand::parse(body)?)),
291            _ => Err(Error::UnexpectedApduTag {
292                got: t.as_u24(),
293                expected: tag::PROFILE_ENQ.as_u24(),
294                what: "resource_manager_v2",
295            }),
296        }
297    }
298}
299
300impl Serialize for ResourceManagerV2Apdu {
301    type Error = Error;
302    fn serialized_len(&self) -> usize {
303        match self {
304            Self::ProfileEnq(o) => o.serialized_len(),
305            Self::ProfileReply(o) => o.serialized_len(),
306            Self::ProfileChanged(o) => o.serialized_len(),
307            Self::ModuleIdSend(o) => o.serialized_len(),
308            Self::ModuleIdCommand(o) => o.serialized_len(),
309        }
310    }
311    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
312        match self {
313            Self::ProfileEnq(o) => o.serialize_into(buf),
314            Self::ProfileReply(o) => o.serialize_into(buf),
315            Self::ProfileChanged(o) => o.serialize_into(buf),
316            Self::ModuleIdSend(o) => o.serialize_into(buf),
317            Self::ModuleIdCommand(o) => o.serialize_into(buf),
318        }
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn profile_enq_round_trips() {
328        let bytes = ProfileEnq.to_bytes();
329        assert_eq!(bytes, [0x9F, 0x80, 0x10, 0x00]);
330        assert_eq!(ProfileEnq::parse(&bytes).unwrap(), ProfileEnq);
331    }
332
333    #[test]
334    fn profile_changed_round_trips() {
335        let bytes = ProfileChanged.to_bytes();
336        assert_eq!(bytes, [0x9F, 0x80, 0x12, 0x00]);
337        assert_eq!(ProfileChanged::parse(&bytes).unwrap(), ProfileChanged);
338    }
339
340    #[test]
341    fn profile_reply_multi_round_trips_and_bites() {
342        let p = ProfileReply {
343            resources: alloc::vec![ResourceId(0x0001_0042), ResourceId(0x0002_0042)],
344        };
345        let bytes = p.to_bytes();
346        // tag(3) + len(1) + 2*4 = 12; len = 0x08.
347        assert_eq!(
348            bytes,
349            [
350                0x9F, 0x80, 0x11, 0x08, 0x00, 0x01, 0x00, 0x42, 0x00, 0x02, 0x00, 0x42
351            ]
352        );
353        assert_eq!(ProfileReply::parse(&bytes).unwrap(), p);
354        let mut other = p.clone();
355        other.resources[1] = ResourceId(0x0022_0041);
356        assert_ne!(bytes, other.to_bytes());
357    }
358
359    #[test]
360    fn module_id_send_round_trips_and_bites() {
361        let m = ModuleIdSend { module_id: 0x03 };
362        let bytes = m.to_bytes();
363        assert_eq!(bytes, [0x9F, 0x80, 0x13, 0x01, 0x03]);
364        assert_eq!(ModuleIdSend::parse(&bytes).unwrap(), m);
365        // top 2 bits ignored on read.
366        let parsed = ModuleIdSend::parse(&[0x9F, 0x80, 0x13, 0x01, 0xC3]).unwrap();
367        assert_eq!(parsed.module_id, 0x03);
368        let other = ModuleIdSend { module_id: 0x04 };
369        assert_ne!(bytes, other.to_bytes());
370    }
371
372    #[test]
373    fn module_id_command_round_trips_and_bites() {
374        let m = ModuleIdCommand {
375            command: ModuleIdCommandKind::SetModuleId,
376            module_id: 0x05,
377        };
378        let bytes = m.to_bytes();
379        assert_eq!(bytes, [0x9F, 0x80, 0x14, 0x02, 0x02, 0x05]);
380        assert_eq!(ModuleIdCommand::parse(&bytes).unwrap(), m);
381        assert_eq!(m.command.name(), "Set_ModuleID");
382        let mut other = m;
383        other.command = ModuleIdCommandKind::Acknowledgement;
384        assert_ne!(bytes, other.to_bytes());
385    }
386
387    #[test]
388    fn dispatch_routes_each_tag() {
389        let enq = ProfileEnq.to_bytes();
390        assert!(matches!(
391            ResourceManagerV2Apdu::parse(&enq).unwrap(),
392            ResourceManagerV2Apdu::ProfileEnq(_)
393        ));
394        let mic = ModuleIdCommand {
395            command: ModuleIdCommandKind::Acknowledgement,
396            module_id: 1,
397        }
398        .to_bytes();
399        let parsed = ResourceManagerV2Apdu::parse(&mic).unwrap();
400        assert!(matches!(parsed, ResourceManagerV2Apdu::ModuleIdCommand(_)));
401        // dispatch enum round-trips.
402        assert_eq!(parsed.to_bytes(), mic);
403    }
404}