Skip to main content

dvb_ci/ci_plus/
file_retrieval.rs

1//! Auxiliary File System resource (CICAM file retrieval) — ETSI TS 103 205
2//! V1.4.1 §9, Tables 72-75 (PDF pp. 96-99). See `docs/ts_103_205/file-retrieval.md`.
3//!
4//! Resource ID `0x00910041` (Class 145, Type 1, Version 1). A generic read-only
5//! mechanism for a CICAM to offer files to the Host (including CICAM broadcast
6//! applications to launch). Tags live in the CI Plus `0x9F94xx` namespace.
7//!
8//! - `FileSystemOffer` (`9F 94 00`, Table 72) — CICAM → Host.
9//! - `FileSystemAck` (`9F 94 01`, Table 73) — Host → CICAM.
10//! - `FileRequest` (`9F 94 02`, §9.4) — Host → CICAM.
11//! - `FileAcknowledge` (`9F 94 03`, §9.5) — CICAM → Host.
12//!
13//! ## Deferred bodies (`FileRequest` / `FileAcknowledge`)
14//!
15//! TS 103 205 §9.4/§9.5 establish only the **tags and direction** of
16//! `FileRequest` (`0x9F9402`) and `FileAcknowledge` (`0x9F9403`); their syntax is
17//! by reference to CI Plus V1.3 \[3\] §14.5.1 / §14.5.2 (proprietary, not
18//! reproduced). We therefore model both as opaque header-only APDUs carrying their
19//! body verbatim as borrowed `&[u8]` ([`FileRequest`] / [`FileAcknowledge`]) — the
20//! field layout is **not invented**. A caller that knows the V1.3 body shape can
21//! decode `body` itself.
22
23use crate::error::{Error, Result};
24use crate::objects;
25use crate::tag::ApduTag;
26use broadcast_common::{Parse, Serialize};
27
28/// Resource-scoped `apdu_tag`s for the Auxiliary File System resource (Table 75).
29pub mod tag {
30    use crate::tag::ApduTag;
31    /// `FilesystemOffer_tag` = `9F 94 00`.
32    pub const FILE_SYSTEM_OFFER: ApduTag = ApduTag::from_bytes(0x9F, 0x94, 0x00);
33    /// `FilesystemAck_tag` = `9F 94 01`.
34    pub const FILE_SYSTEM_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x94, 0x01);
35    /// `FileRequest_tag` = `9F 94 02` (body deferred to CI Plus V1.3 §14.5.1).
36    pub const FILE_REQUEST: ApduTag = ApduTag::from_bytes(0x9F, 0x94, 0x02);
37    /// `FileAcknowledge_tag` = `9F 94 03` (body deferred to CI Plus V1.3 §14.5.2).
38    pub const FILE_ACKNOWLEDGE: ApduTag = ApduTag::from_bytes(0x9F, 0x94, 0x03);
39}
40
41// --- AckCode (Table 74) ---
42
43/// `AckCode` values (Table 74), the Host's response to a [`FileSystemOffer`].
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize))]
46#[non_exhaustive]
47pub enum AckCode {
48    /// `0x01` — OK, the application environment is supported by the Host.
49    Ok,
50    /// `0x02` — Unknown DomainIdentifier, not supported by the Host.
51    UnknownDomainIdentifier,
52    /// Reserved (`0x00`, `0x03`–`0xFF`).
53    Reserved(u8),
54}
55impl AckCode {
56    /// Decode an `AckCode` byte.
57    #[must_use]
58    pub fn from_u8(v: u8) -> Self {
59        match v {
60            0x01 => Self::Ok,
61            0x02 => Self::UnknownDomainIdentifier,
62            other => Self::Reserved(other),
63        }
64    }
65    /// Wire byte.
66    #[must_use]
67    pub const fn to_u8(self) -> u8 {
68        match self {
69            Self::Ok => 0x01,
70            Self::UnknownDomainIdentifier => 0x02,
71            Self::Reserved(v) => v,
72        }
73    }
74    /// Spec token, or `"reserved"`.
75    #[must_use]
76    pub fn name(&self) -> &'static str {
77        match self {
78            Self::Ok => "ok",
79            Self::UnknownDomainIdentifier => "unknown_domain_identifier",
80            Self::Reserved(_) => "reserved",
81        }
82    }
83}
84broadcast_common::impl_spec_display!(AckCode, Reserved);
85
86// ---------------------------------------------------------------------------
87// FileSystemOffer (Table 72)
88// ---------------------------------------------------------------------------
89
90/// `FileSystemOffer()` (Table 72): CICAM → Host. Specifies the file system
91/// provided by the CICAM.
92#[derive(Debug, Clone, PartialEq, Eq)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize))]
94pub struct FileSystemOffer<'a> {
95    /// `DomainIdentifier` body (`DomainIdentifierLength` bytes) — opaque
96    /// middleware-defined identifier (URL / UUID / DVB-registered id).
97    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
98    pub domain_identifier: &'a [u8],
99}
100
101// DomainIdentifierLength(1) + DomainIdentifier bytes.
102const OFFER_PREFIX: usize = 1;
103
104impl<'a> Parse<'a> for FileSystemOffer<'a> {
105    type Error = Error;
106    fn parse(bytes: &'a [u8]) -> Result<Self> {
107        let body = objects::parse_apdu_header(bytes, tag::FILE_SYSTEM_OFFER, "FileSystemOffer")?;
108        if body.len() < OFFER_PREFIX {
109            return Err(Error::BufferTooShort {
110                need: OFFER_PREFIX,
111                have: body.len(),
112                what: "FileSystemOffer",
113            });
114        }
115        let len = body[0] as usize;
116        let end = OFFER_PREFIX + len;
117        if body.len() < end {
118            return Err(Error::LengthMismatch {
119                what: "FileSystemOffer DomainIdentifier",
120                declared: len,
121                actual: body.len().saturating_sub(OFFER_PREFIX),
122            });
123        }
124        Ok(Self {
125            domain_identifier: &body[OFFER_PREFIX..end],
126        })
127    }
128}
129impl Serialize for FileSystemOffer<'_> {
130    type Error = Error;
131    fn serialized_len(&self) -> usize {
132        objects::apdu_len(OFFER_PREFIX + self.domain_identifier.len())
133    }
134    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
135        let body_len = OFFER_PREFIX + self.domain_identifier.len();
136        let pos = objects::write_apdu_header(tag::FILE_SYSTEM_OFFER, body_len, buf)?;
137        buf[pos] = self.domain_identifier.len() as u8;
138        buf[pos + OFFER_PREFIX..pos + body_len].copy_from_slice(self.domain_identifier);
139        Ok(pos + body_len)
140    }
141}
142
143// ---------------------------------------------------------------------------
144// FileSystemAck (Table 73)
145// ---------------------------------------------------------------------------
146
147/// `FileSystemAck()` (Table 73): Host → CICAM.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149#[cfg_attr(feature = "serde", derive(serde::Serialize))]
150pub struct FileSystemAck {
151    /// `AckCode` (8) — Table 74.
152    pub ack_code: AckCode,
153}
154
155const ACK_BODY: usize = 1;
156
157impl<'a> Parse<'a> for FileSystemAck {
158    type Error = Error;
159    fn parse(bytes: &'a [u8]) -> Result<Self> {
160        let body = objects::parse_apdu_header(bytes, tag::FILE_SYSTEM_ACK, "FileSystemAck")?;
161        if body.len() < ACK_BODY {
162            return Err(Error::BufferTooShort {
163                need: ACK_BODY,
164                have: body.len(),
165                what: "FileSystemAck",
166            });
167        }
168        Ok(Self {
169            ack_code: AckCode::from_u8(body[0]),
170        })
171    }
172}
173impl Serialize for FileSystemAck {
174    type Error = Error;
175    fn serialized_len(&self) -> usize {
176        objects::apdu_len(ACK_BODY)
177    }
178    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
179        let pos = objects::write_apdu_header(tag::FILE_SYSTEM_ACK, ACK_BODY, buf)?;
180        buf[pos] = self.ack_code.to_u8();
181        Ok(pos + ACK_BODY)
182    }
183}
184
185// ---------------------------------------------------------------------------
186// FileRequest (§9.4) / FileAcknowledge (§9.5) — bodies deferred to CI Plus V1.3
187// ---------------------------------------------------------------------------
188
189/// `FileRequest()` (§9.4, tag `0x9F9402`): Host → CICAM. **Body deferred** to
190/// CI Plus V1.3 \[3\] §14.5.1 (proprietary, not reproduced in TS 103 205) — the
191/// body is carried verbatim as opaque borrowed bytes; the field layout is not
192/// invented.
193#[derive(Debug, Clone, PartialEq, Eq)]
194#[cfg_attr(feature = "serde", derive(serde::Serialize))]
195pub struct FileRequest<'a> {
196    /// Opaque CI Plus V1.3 §14.5.1 body (whole `length_field()` body, verbatim).
197    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
198    pub body: &'a [u8],
199}
200
201impl<'a> Parse<'a> for FileRequest<'a> {
202    type Error = Error;
203    fn parse(bytes: &'a [u8]) -> Result<Self> {
204        let body = objects::parse_apdu_header(bytes, tag::FILE_REQUEST, "FileRequest")?;
205        Ok(Self { body })
206    }
207}
208impl Serialize for FileRequest<'_> {
209    type Error = Error;
210    fn serialized_len(&self) -> usize {
211        objects::apdu_len(self.body.len())
212    }
213    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
214        let pos = objects::write_apdu_header(tag::FILE_REQUEST, self.body.len(), buf)?;
215        buf[pos..pos + self.body.len()].copy_from_slice(self.body);
216        Ok(pos + self.body.len())
217    }
218}
219
220/// `FileAcknowledge()` (§9.5, tag `0x9F9403`): CICAM → Host. **Body deferred** to
221/// CI Plus V1.3 \[3\] §14.5.2 (proprietary, not reproduced in TS 103 205) — the
222/// body is carried verbatim as opaque borrowed bytes; the field layout is not
223/// invented.
224#[derive(Debug, Clone, PartialEq, Eq)]
225#[cfg_attr(feature = "serde", derive(serde::Serialize))]
226pub struct FileAcknowledge<'a> {
227    /// Opaque CI Plus V1.3 §14.5.2 body (whole `length_field()` body, verbatim).
228    #[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
229    pub body: &'a [u8],
230}
231
232impl<'a> Parse<'a> for FileAcknowledge<'a> {
233    type Error = Error;
234    fn parse(bytes: &'a [u8]) -> Result<Self> {
235        let body = objects::parse_apdu_header(bytes, tag::FILE_ACKNOWLEDGE, "FileAcknowledge")?;
236        Ok(Self { body })
237    }
238}
239impl Serialize for FileAcknowledge<'_> {
240    type Error = Error;
241    fn serialized_len(&self) -> usize {
242        objects::apdu_len(self.body.len())
243    }
244    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
245        let pos = objects::write_apdu_header(tag::FILE_ACKNOWLEDGE, self.body.len(), buf)?;
246        buf[pos..pos + self.body.len()].copy_from_slice(self.body);
247        Ok(pos + self.body.len())
248    }
249}
250
251// ---------------------------------------------------------------------------
252// Resource-scoped dispatch
253// ---------------------------------------------------------------------------
254
255/// Resource-scoped dispatch over the Auxiliary File System resource objects.
256#[derive(Debug, Clone, PartialEq, Eq)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize))]
258#[non_exhaustive]
259pub enum FileRetrievalApdu<'a> {
260    /// `FileSystemOffer` (`9F 94 00`).
261    FileSystemOffer(#[cfg_attr(feature = "serde", serde(borrow))] FileSystemOffer<'a>),
262    /// `FileSystemAck` (`9F 94 01`).
263    FileSystemAck(FileSystemAck),
264    /// `FileRequest` (`9F 94 02`) — opaque deferred body.
265    FileRequest(#[cfg_attr(feature = "serde", serde(borrow))] FileRequest<'a>),
266    /// `FileAcknowledge` (`9F 94 03`) — opaque deferred body.
267    FileAcknowledge(#[cfg_attr(feature = "serde", serde(borrow))] FileAcknowledge<'a>),
268}
269
270impl<'a> FileRetrievalApdu<'a> {
271    /// Parse an Auxiliary File System APDU, dispatching on the leading `apdu_tag`.
272    pub fn parse(body: &'a [u8]) -> Result<Self> {
273        if body.len() < 3 {
274            return Err(Error::BufferTooShort {
275                need: 3,
276                have: body.len(),
277                what: "file_retrieval apdu_tag",
278            });
279        }
280        let t = ApduTag::from_bytes(body[0], body[1], body[2]);
281        match t {
282            tag::FILE_SYSTEM_OFFER => Ok(Self::FileSystemOffer(FileSystemOffer::parse(body)?)),
283            tag::FILE_SYSTEM_ACK => Ok(Self::FileSystemAck(FileSystemAck::parse(body)?)),
284            tag::FILE_REQUEST => Ok(Self::FileRequest(FileRequest::parse(body)?)),
285            tag::FILE_ACKNOWLEDGE => Ok(Self::FileAcknowledge(FileAcknowledge::parse(body)?)),
286            _ => Err(Error::UnexpectedApduTag {
287                got: t.as_u24(),
288                expected: tag::FILE_SYSTEM_OFFER.as_u24(),
289                what: "file_retrieval",
290            }),
291        }
292    }
293}
294
295impl Serialize for FileRetrievalApdu<'_> {
296    type Error = Error;
297    fn serialized_len(&self) -> usize {
298        match self {
299            Self::FileSystemOffer(o) => o.serialized_len(),
300            Self::FileSystemAck(o) => o.serialized_len(),
301            Self::FileRequest(o) => o.serialized_len(),
302            Self::FileAcknowledge(o) => o.serialized_len(),
303        }
304    }
305    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
306        match self {
307            Self::FileSystemOffer(o) => o.serialize_into(buf),
308            Self::FileSystemAck(o) => o.serialize_into(buf),
309            Self::FileRequest(o) => o.serialize_into(buf),
310            Self::FileAcknowledge(o) => o.serialize_into(buf),
311        }
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn offer_round_trips_and_bites() {
321        // DomainIdentifier "ab".
322        let o = FileSystemOffer {
323            domain_identifier: &[0x61, 0x62],
324        };
325        let bytes = o.to_bytes();
326        // tag(9F 94 00) len(03) DomainIdentifierLength(02) 61 62.
327        assert_eq!(bytes, [0x9F, 0x94, 0x00, 0x03, 0x02, 0x61, 0x62]);
328        assert_eq!(FileSystemOffer::parse(&bytes).unwrap(), o);
329        let other = FileSystemOffer {
330            domain_identifier: &[0x61, 0x63],
331        };
332        assert_ne!(bytes, other.to_bytes());
333    }
334
335    #[test]
336    fn offer_empty_domain() {
337        let o = FileSystemOffer {
338            domain_identifier: &[],
339        };
340        let bytes = o.to_bytes();
341        assert_eq!(bytes, [0x9F, 0x94, 0x00, 0x01, 0x00]);
342        assert_eq!(FileSystemOffer::parse(&bytes).unwrap(), o);
343    }
344
345    #[test]
346    fn ack_round_trips() {
347        let a = FileSystemAck {
348            ack_code: AckCode::UnknownDomainIdentifier,
349        };
350        let bytes = a.to_bytes();
351        assert_eq!(bytes, [0x9F, 0x94, 0x01, 0x01, 0x02]);
352        assert_eq!(FileSystemAck::parse(&bytes).unwrap(), a);
353        let ok = FileSystemAck {
354            ack_code: AckCode::Ok,
355        };
356        assert_eq!(ok.to_bytes()[4], 0x01);
357    }
358
359    #[test]
360    fn file_request_opaque_body_round_trips() {
361        let r = FileRequest {
362            body: &[0x01, 0x02, 0x03],
363        };
364        let bytes = r.to_bytes();
365        assert_eq!(bytes, [0x9F, 0x94, 0x02, 0x03, 0x01, 0x02, 0x03]);
366        assert_eq!(FileRequest::parse(&bytes).unwrap(), r);
367    }
368
369    #[test]
370    fn file_acknowledge_opaque_body_round_trips() {
371        let a = FileAcknowledge {
372            body: &[0xAA, 0xBB],
373        };
374        let bytes = a.to_bytes();
375        assert_eq!(bytes, [0x9F, 0x94, 0x03, 0x02, 0xAA, 0xBB]);
376        assert_eq!(FileAcknowledge::parse(&bytes).unwrap(), a);
377    }
378
379    #[test]
380    fn dispatch_routes_each_tag() {
381        let cases: alloc::vec::Vec<alloc::vec::Vec<u8>> = alloc::vec![
382            FileSystemOffer {
383                domain_identifier: &[0x61]
384            }
385            .to_bytes(),
386            FileSystemAck {
387                ack_code: AckCode::Ok
388            }
389            .to_bytes(),
390            FileRequest { body: &[0x00] }.to_bytes(),
391            FileAcknowledge { body: &[0x00] }.to_bytes(),
392        ];
393        for c in &cases {
394            assert_eq!(&FileRetrievalApdu::parse(c).unwrap().to_bytes(), c);
395        }
396        assert!(matches!(
397            FileRetrievalApdu::parse(&[0x9F, 0x94, 0x7E, 0x00]),
398            Err(Error::UnexpectedApduTag { .. })
399        ));
400    }
401}