Skip to main content

dvb_si/descriptors/extension/
cpcm_delivery_signalling.rs

1//! CPCM Delivery Signalling Descriptor — ETSI TS 102 825-9 §4.1.5, Table 2
2//! (tag_extension 0x01); typed CPCM USI decode per ETSI TS 102 825-4 §5.4 Table 8.
3use super::*;
4use alloc::vec::Vec;
5
6impl<'a> ExtensionBodyDef<'a> for CpcmDeliverySignalling<'a> {
7    const TAG_EXTENSION: u8 = 0x01;
8    const NAME: &'static str = "CPCM_DELIVERY_SIGNALLING";
9}
10
11/// cpcm_delivery_signalling body (Table 2, §4.1.5): an encoding version plus the
12/// version-dependent CPCM USI `selector_byte`s. For `cpcm_version == 1` the
13/// selector is the CPCM delivery signalling (USI) of ETSI TS 102 825-4; at the
14/// descriptor level it is a version-tagged opaque payload.
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize))]
17#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
18pub struct CpcmDeliverySignalling<'a> {
19    /// `cpcm_version` — encoding version of the USI structure in the selector bytes.
20    pub cpcm_version: u8,
21    /// The `selector_byte`s (version-dependent CPCM USI payload; see TS 102 825-4).
22    #[cfg_attr(feature = "serde", serde(borrow))]
23    pub selector_bytes: &'a [u8],
24}
25
26impl<'a> CpcmDeliverySignalling<'a> {
27    /// Attempt to decode the selector bytes as a typed [`CpcmUsi`].
28    ///
29    /// Returns `Some(Ok(_))` when `cpcm_version == 1` and the selector parses
30    /// successfully; `Some(Err(_))` when version is 1 but the bytes are malformed;
31    /// `None` when `cpcm_version != 1`.
32    #[must_use]
33    pub fn usi(&self) -> Option<Result<CpcmUsi>> {
34        if self.cpcm_version == 1 {
35            Some(CpcmUsi::parse(self.selector_bytes))
36        } else {
37            None
38        }
39    }
40}
41
42impl<'a> Parse<'a> for CpcmDeliverySignalling<'a> {
43    type Error = crate::error::Error;
44    fn parse(sel: &'a [u8]) -> Result<Self> {
45        let (cpcm_version, selector_bytes) = sel.split_first().ok_or(Error::BufferTooShort {
46            need: 1,
47            have: 0,
48            what: "cpcm_delivery_signalling body",
49        })?;
50        Ok(CpcmDeliverySignalling {
51            cpcm_version: *cpcm_version,
52            selector_bytes,
53        })
54    }
55}
56
57impl Serialize for CpcmDeliverySignalling<'_> {
58    type Error = crate::error::Error;
59    fn serialized_len(&self) -> usize {
60        1 + self.selector_bytes.len()
61    }
62    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
63        let len = self.serialized_len();
64        if buf.len() < len {
65            return Err(Error::OutputBufferTooSmall {
66                need: len,
67                have: buf.len(),
68            });
69        }
70        buf[0] = self.cpcm_version;
71        buf[1..len].copy_from_slice(self.selector_bytes);
72        Ok(len)
73    }
74}
75
76// ── CpcmUsi ──────────────────────────────────────────────────────────────────
77
78/// `copy_control` (cci_and_zero_retention) — ETSI TS 102 825-4 Table 9.
79///
80/// Coded as a 3-bit `uimsbf` in byte 1 `[7:5]` of the USI.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize))]
83#[non_exhaustive]
84#[repr(u8)]
85pub enum CopyControl {
86    /// 0 — Copy Control Not Asserted.
87    CopyControlNotAsserted = 0,
88    /// 1 — Copy Once.
89    CopyOnce = 1,
90    /// 2 — Copy No More.
91    CopyNoMore = 2,
92    /// 3 — Copy Never — Zero Retention Not Asserted.
93    CopyNeverZeroRetentionNotAsserted = 3,
94    /// 4 — Copy Never — Zero Retention Asserted.
95    CopyNeverZeroRetentionAsserted = 4,
96    /// 5–7 — Reserved for future use.
97    Reserved(u8) = 5,
98}
99
100impl CopyControl {
101    /// Construct from the raw 3-bit value; values 5–7 map to `Reserved(v)`.
102    #[must_use]
103    pub fn from_u8(v: u8) -> Self {
104        match v & 0x07 {
105            0 => CopyControl::CopyControlNotAsserted,
106            1 => CopyControl::CopyOnce,
107            2 => CopyControl::CopyNoMore,
108            3 => CopyControl::CopyNeverZeroRetentionNotAsserted,
109            4 => CopyControl::CopyNeverZeroRetentionAsserted,
110            r => CopyControl::Reserved(r),
111        }
112    }
113
114    /// Return the raw 3-bit value.
115    #[must_use]
116    pub const fn to_u8(self) -> u8 {
117        match self {
118            CopyControl::CopyControlNotAsserted => 0,
119            CopyControl::CopyOnce => 1,
120            CopyControl::CopyNoMore => 2,
121            CopyControl::CopyNeverZeroRetentionNotAsserted => 3,
122            CopyControl::CopyNeverZeroRetentionAsserted => 4,
123            CopyControl::Reserved(r) => r,
124        }
125    }
126
127    /// Spec label per Table 9.
128    #[must_use]
129    pub fn name(self) -> &'static str {
130        match self {
131            CopyControl::CopyControlNotAsserted => "Copy Control Not Asserted",
132            CopyControl::CopyOnce => "Copy Once",
133            CopyControl::CopyNoMore => "Copy No More",
134            CopyControl::CopyNeverZeroRetentionNotAsserted => {
135                "Copy Never - Zero Retention Not Asserted"
136            }
137            CopyControl::CopyNeverZeroRetentionAsserted => "Copy Never - Zero Retention Asserted",
138            CopyControl::Reserved(_) => "reserved",
139        }
140    }
141}
142dvb_common::impl_spec_display!(CopyControl, Reserved);
143
144/// `move_and_copy_propagation_information` — ETSI TS 102 825-4 Table 10.
145///
146/// Coded as a 2-bit `uimsbf` in byte 2 `[5:4]` of the USI.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize))]
149#[non_exhaustive]
150#[repr(u8)]
151pub enum MoveCopyPropagation {
152    /// 0 — MLAD: copying/movement within the same Localized AD is allowed.
153    Mlad = 0,
154    /// 1 — MGAD: copying/movement within the same Geographically-constrained AD is allowed.
155    Mgad = 1,
156    /// 2 — MAD: copying/movement within the same Authorized Domain is allowed.
157    Mad = 2,
158    /// 3 — MCPCM: copying/movement to any CPCM-compliant Storage Entity is allowed.
159    Mcpcm = 3,
160}
161
162impl MoveCopyPropagation {
163    /// Construct from the raw 2-bit value; the field is total (values 0–3 exhaustive).
164    #[must_use]
165    pub fn from_u8(v: u8) -> Self {
166        match v & 0x03 {
167            0 => MoveCopyPropagation::Mlad,
168            1 => MoveCopyPropagation::Mgad,
169            2 => MoveCopyPropagation::Mad,
170            _ => MoveCopyPropagation::Mcpcm,
171        }
172    }
173
174    /// Return the raw 2-bit value.
175    #[must_use]
176    pub const fn to_u8(self) -> u8 {
177        self as u8
178    }
179
180    /// Spec label per Table 10.
181    #[must_use]
182    pub fn name(self) -> &'static str {
183        match self {
184            MoveCopyPropagation::Mlad => "MLAD",
185            MoveCopyPropagation::Mgad => "MGAD",
186            MoveCopyPropagation::Mad => "MAD",
187            MoveCopyPropagation::Mcpcm => "MCPCM",
188        }
189    }
190}
191dvb_common::impl_spec_display!(MoveCopyPropagation);
192
193/// `view_propagation_information` — ETSI TS 102 825-4 Table 11.
194///
195/// Coded as a 2-bit `uimsbf` in byte 2 `[3:2]` of the USI.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197#[cfg_attr(feature = "serde", derive(serde::Serialize))]
198#[non_exhaustive]
199#[repr(u8)]
200pub enum ViewPropagation {
201    /// 0 — VLAD: consumption within the same Localized AD is allowed.
202    Vlad = 0,
203    /// 1 — VGAD: consumption within the same Geographically-constrained AD is allowed.
204    Vgad = 1,
205    /// 2 — VAD: consumption within the same Authorized Domain is allowed.
206    Vad = 2,
207    /// 3 — VCPCM: consumption using any CPCM-compliant Consumption Point is allowed.
208    Vcpcm = 3,
209}
210
211impl ViewPropagation {
212    /// Construct from the raw 2-bit value; the field is total (values 0–3 exhaustive).
213    #[must_use]
214    pub fn from_u8(v: u8) -> Self {
215        match v & 0x03 {
216            0 => ViewPropagation::Vlad,
217            1 => ViewPropagation::Vgad,
218            2 => ViewPropagation::Vad,
219            _ => ViewPropagation::Vcpcm,
220        }
221    }
222
223    /// Return the raw 2-bit value.
224    #[must_use]
225    pub const fn to_u8(self) -> u8 {
226        self as u8
227    }
228
229    /// Spec label per Table 11.
230    #[must_use]
231    pub fn name(self) -> &'static str {
232        match self {
233            ViewPropagation::Vlad => "VLAD",
234            ViewPropagation::Vgad => "VGAD",
235            ViewPropagation::Vad => "VAD",
236            ViewPropagation::Vcpcm => "VCPCM",
237        }
238    }
239}
240dvb_common::impl_spec_display!(ViewPropagation);
241
242/// One entry in the `export_controlled_cps` CPS vector (Table 8, §5.4).
243///
244/// Layout per entry: `C_and_R_regime_mask`(8) || `cps_vector_length`(16) ||
245/// `cps_vector_byte`\[cps_vector_length\].
246#[derive(Debug, Clone, PartialEq, Eq)]
247#[cfg_attr(feature = "serde", derive(serde::Serialize))]
248pub struct CpsVectorEntry {
249    /// `C_and_R_regime_mask` — identifies which C&R regimes this vector applies to.
250    pub c_and_r_regime_mask: u8,
251    /// `cps_vector_byte` payload — length encoded as `cps_vector_length`(16).
252    pub cps_vector: Vec<u8>,
253}
254
255// Fixed-byte widths within USI (ETSI TS 102 825-4 Table 8).
256/// Byte width of the 3 flag bytes (bytes 1–3 after `length`).
257const USI_FLAGS_LEN: usize = 3;
258/// Byte width of `CPCM_date_time` (40-bit opaque timestamp, Table 8).
259const CPCM_DATE_TIME_LEN: usize = 5;
260/// Byte width of `CPCM_playback_period` (16-bit opaque value, Table 8).
261const CPCM_PLAYBACK_PERIOD_LEN: usize = 2;
262/// Byte width of `simultaneous_view_count` (8-bit uimsbf).
263const SIMULTANEOUS_VIEW_COUNT_LEN: usize = 1;
264/// Byte width of the per-CPS-entry header before the variable payload
265/// (`C_and_R_regime_mask`(1) + `cps_vector_length`(2)).
266const CPS_ENTRY_HDR_LEN: usize = 3;
267
268/// Typed decode of `CPCM_usage_state_information()` — ETSI TS 102 825-4 §5.4 Table 8.
269///
270/// This struct mirrors the wire layout exactly:
271///
272/// ```text
273/// byte 0     length (bytes following this field)
274/// byte 1     copy_control[7:5] | do_not_cpcm_scramble[4] | viewable[3]
275///            | view_window_activated[2] | view_period_activated[1]
276///            | simultaneous_view_count_activated[0]
277/// byte 2     move_local[7] | view_local[6]
278///            | move_and_copy_propagation_information[5:4]
279///            | view_propagation_information[3:2]
280///            | remote_access_date_moving_window_flag[1]
281///            | remote_access_date_immediate_flag[0]
282/// byte 3     remote_access_record_flag[7] | export_controlled_cps[6]
283///            | export_beyond_trust[5] | disable_analogue_sd_export[4]
284///            | disable_analogue_sd_consumption[3] | disable_analogue_hd_export[2]
285///            | disable_analogue_hd_consumption[1] | image_constraint[0]
286/// then conditional fields in declaration order (§5.4)
287/// ```
288///
289/// Obtain via [`CpcmDeliverySignalling::usi()`].
290#[derive(Debug, Clone, PartialEq, Eq)]
291#[cfg_attr(feature = "serde", derive(serde::Serialize))]
292pub struct CpcmUsi {
293    // ── byte 1 ───────────────────────────────────────────────────────────────
294    /// `copy_control`(3) — cci_and_zero_retention per Table 9.
295    pub copy_control: CopyControl,
296    /// `do_not_cpcm_scramble`(1) — 1 = do not apply CPCM scrambling.
297    pub do_not_cpcm_scramble: bool,
298    /// `viewable`(1) — 1 = consumption and export enabled.
299    pub viewable: bool,
300    /// `view_window_activated`(1) — gates `view_window_start`/`view_window_end`.
301    pub view_window_activated: bool,
302    /// `view_period_activated`(1) — gates `view_period_from_first_playback`.
303    pub view_period_activated: bool,
304    /// `simultaneous_view_count_activated`(1) — gates `simultaneous_view_count`.
305    pub simultaneous_view_count_activated: bool,
306    // ── byte 2 ───────────────────────────────────────────────────────────────
307    /// `move_local`(1) — copying/movement allowed if destination is local.
308    pub move_local: bool,
309    /// `view_local`(1) — consumption allowed if consumption point is local.
310    pub view_local: bool,
311    /// `move_and_copy_propagation_information`(2) — per Table 10.
312    pub move_and_copy_propagation_information: MoveCopyPropagation,
313    /// `view_propagation_information`(2) — per Table 11.
314    pub view_propagation_information: ViewPropagation,
315    /// `remote_access_date_moving_window_flag`(1) — gates `remote_access_date`.
316    pub remote_access_date_moving_window_flag: bool,
317    /// `remote_access_date_immediate_flag`(1) — gates `remote_access_date`.
318    pub remote_access_date_immediate_flag: bool,
319    // ── byte 3 ───────────────────────────────────────────────────────────────
320    /// `remote_access_record_flag`(1).
321    pub remote_access_record_flag: bool,
322    /// `export_controlled_cps`(1) — gates the CPS vector.
323    pub export_controlled_cps: bool,
324    /// `export_beyond_trust`(1) — content may be exported to an untrusted space.
325    pub export_beyond_trust: bool,
326    /// `disable_analogue_sd_export`(1).
327    pub disable_analogue_sd_export: bool,
328    /// `disable_analogue_sd_consumption`(1).
329    pub disable_analogue_sd_consumption: bool,
330    /// `disable_analogue_hd_export`(1).
331    pub disable_analogue_hd_export: bool,
332    /// `disable_analogue_hd_consumption`(1).
333    pub disable_analogue_hd_consumption: bool,
334    /// `image_constraint`(1) — HD images shall be rendered at lower resolutions.
335    pub image_constraint: bool,
336    // ── conditional fields (in declaration order, §5.4) ──────────────────────
337    /// `view_window_start` (40-bit `CPCM_date_time`) — present iff `view_window_activated`.
338    pub view_window_start: Option<[u8; CPCM_DATE_TIME_LEN]>,
339    /// `view_window_end` (40-bit `CPCM_date_time`) — present iff `view_window_activated`.
340    pub view_window_end: Option<[u8; CPCM_DATE_TIME_LEN]>,
341    /// `view_period_from_first_playback` (16-bit `CPCM_playback_period`) — present iff
342    /// `view_period_activated`.
343    pub view_period_from_first_playback: Option<[u8; CPCM_PLAYBACK_PERIOD_LEN]>,
344    /// `simultaneous_view_count` (8-bit uimsbf) — present iff
345    /// `simultaneous_view_count_activated`.
346    pub simultaneous_view_count: Option<u8>,
347    /// `remote_access_date` (40-bit `CPCM_date_time`) — present iff
348    /// `remote_access_date_immediate_flag || remote_access_date_moving_window_flag`.
349    pub remote_access_date: Option<[u8; CPCM_DATE_TIME_LEN]>,
350    /// CPS vector entries — present (non-empty) iff `export_controlled_cps`.
351    pub cps_vectors: Vec<CpsVectorEntry>,
352}
353
354impl<'a> Parse<'a> for CpcmUsi {
355    type Error = crate::error::Error;
356
357    fn parse(bytes: &'a [u8]) -> Result<Self> {
358        // byte 0: length (counts bytes after itself)
359        if bytes.is_empty() {
360            return Err(Error::BufferTooShort {
361                need: 1,
362                have: 0,
363                what: "CpcmUsi length",
364            });
365        }
366        let length = bytes[0] as usize;
367        // Total bytes available: 1 (length byte) + length.
368        // We need at least 1 + USI_FLAGS_LEN (3) = 4 bytes.
369        if bytes.len() < 1 + USI_FLAGS_LEN {
370            return Err(Error::BufferTooShort {
371                need: 1 + USI_FLAGS_LEN,
372                have: bytes.len(),
373                what: "CpcmUsi fixed flags",
374            });
375        }
376        // The `length` field describes how many bytes follow it.
377        // We validate that the slice is exactly that long.
378        if bytes.len() < 1 + length {
379            return Err(Error::BufferTooShort {
380                need: 1 + length,
381                have: bytes.len(),
382                what: "CpcmUsi body",
383            });
384        }
385        // Work within the declared extent: bytes[1 .. 1+length].
386        let body = &bytes[1..1 + length];
387        if body.len() < USI_FLAGS_LEN {
388            return Err(Error::BufferTooShort {
389                need: USI_FLAGS_LEN,
390                have: body.len(),
391                what: "CpcmUsi flag bytes",
392            });
393        }
394
395        // ── byte 1 ─────────────────────────────────────────────────────────
396        let b1 = body[0];
397        let copy_control = CopyControl::from_u8(b1 >> 5);
398        let do_not_cpcm_scramble = (b1 >> 4) & 1 != 0;
399        let viewable = (b1 >> 3) & 1 != 0;
400        let view_window_activated = (b1 >> 2) & 1 != 0;
401        let view_period_activated = (b1 >> 1) & 1 != 0;
402        let simultaneous_view_count_activated = b1 & 1 != 0;
403
404        // ── byte 2 ─────────────────────────────────────────────────────────
405        let b2 = body[1];
406        let move_local = (b2 >> 7) & 1 != 0;
407        let view_local = (b2 >> 6) & 1 != 0;
408        let move_and_copy_propagation_information = MoveCopyPropagation::from_u8(b2 >> 4);
409        let view_propagation_information = ViewPropagation::from_u8(b2 >> 2);
410        let remote_access_date_moving_window_flag = (b2 >> 1) & 1 != 0;
411        let remote_access_date_immediate_flag = b2 & 1 != 0;
412
413        // ── byte 3 ─────────────────────────────────────────────────────────
414        let b3 = body[2];
415        let remote_access_record_flag = (b3 >> 7) & 1 != 0;
416        let export_controlled_cps = (b3 >> 6) & 1 != 0;
417        let export_beyond_trust = (b3 >> 5) & 1 != 0;
418        let disable_analogue_sd_export = (b3 >> 4) & 1 != 0;
419        let disable_analogue_sd_consumption = (b3 >> 3) & 1 != 0;
420        let disable_analogue_hd_export = (b3 >> 2) & 1 != 0;
421        let disable_analogue_hd_consumption = (b3 >> 1) & 1 != 0;
422        let image_constraint = b3 & 1 != 0;
423
424        // ── conditional fields ──────────────────────────────────────────────
425        let mut pos = USI_FLAGS_LEN; // position within `body`
426
427        let view_window_start;
428        let view_window_end;
429        if view_window_activated {
430            if body.len() < pos + CPCM_DATE_TIME_LEN * 2 {
431                return Err(Error::BufferTooShort {
432                    need: pos + CPCM_DATE_TIME_LEN * 2,
433                    have: body.len(),
434                    what: "CpcmUsi view_window_start/end",
435                });
436            }
437            let mut start = [0u8; CPCM_DATE_TIME_LEN];
438            start.copy_from_slice(&body[pos..pos + CPCM_DATE_TIME_LEN]);
439            pos += CPCM_DATE_TIME_LEN;
440            let mut end = [0u8; CPCM_DATE_TIME_LEN];
441            end.copy_from_slice(&body[pos..pos + CPCM_DATE_TIME_LEN]);
442            pos += CPCM_DATE_TIME_LEN;
443            view_window_start = Some(start);
444            view_window_end = Some(end);
445        } else {
446            view_window_start = None;
447            view_window_end = None;
448        }
449
450        let view_period_from_first_playback = if view_period_activated {
451            if body.len() < pos + CPCM_PLAYBACK_PERIOD_LEN {
452                return Err(Error::BufferTooShort {
453                    need: pos + CPCM_PLAYBACK_PERIOD_LEN,
454                    have: body.len(),
455                    what: "CpcmUsi view_period_from_first_playback",
456                });
457            }
458            let mut vp = [0u8; CPCM_PLAYBACK_PERIOD_LEN];
459            vp.copy_from_slice(&body[pos..pos + CPCM_PLAYBACK_PERIOD_LEN]);
460            pos += CPCM_PLAYBACK_PERIOD_LEN;
461            Some(vp)
462        } else {
463            None
464        };
465
466        let simultaneous_view_count;
467        if simultaneous_view_count_activated {
468            if body.len() < pos + SIMULTANEOUS_VIEW_COUNT_LEN {
469                return Err(Error::BufferTooShort {
470                    need: pos + SIMULTANEOUS_VIEW_COUNT_LEN,
471                    have: body.len(),
472                    what: "CpcmUsi simultaneous_view_count",
473                });
474            }
475            simultaneous_view_count = Some(body[pos]);
476            pos += SIMULTANEOUS_VIEW_COUNT_LEN;
477        } else {
478            simultaneous_view_count = None;
479        }
480
481        let remote_access_date =
482            if remote_access_date_immediate_flag || remote_access_date_moving_window_flag {
483                if body.len() < pos + CPCM_DATE_TIME_LEN {
484                    return Err(Error::BufferTooShort {
485                        need: pos + CPCM_DATE_TIME_LEN,
486                        have: body.len(),
487                        what: "CpcmUsi remote_access_date",
488                    });
489                }
490                let mut rad = [0u8; CPCM_DATE_TIME_LEN];
491                rad.copy_from_slice(&body[pos..pos + CPCM_DATE_TIME_LEN]);
492                pos += CPCM_DATE_TIME_LEN;
493                Some(rad)
494            } else {
495                None
496            };
497
498        let mut cps_vectors = Vec::new();
499        if export_controlled_cps {
500            if body.len() < pos + 1 {
501                return Err(Error::BufferTooShort {
502                    need: pos + 1,
503                    have: body.len(),
504                    what: "CpcmUsi cps_vector_count",
505                });
506            }
507            let cps_vector_count = body[pos] as usize;
508            pos += 1;
509            for _ in 0..cps_vector_count {
510                if body.len() < pos + CPS_ENTRY_HDR_LEN {
511                    return Err(Error::BufferTooShort {
512                        need: pos + CPS_ENTRY_HDR_LEN,
513                        have: body.len(),
514                        what: "CpcmUsi CPS entry header",
515                    });
516                }
517                let c_and_r_regime_mask = body[pos];
518                let cps_vector_length = u16::from_be_bytes([body[pos + 1], body[pos + 2]]) as usize;
519                pos += CPS_ENTRY_HDR_LEN;
520                if body.len() < pos + cps_vector_length {
521                    return Err(Error::BufferTooShort {
522                        need: pos + cps_vector_length,
523                        have: body.len(),
524                        what: "CpcmUsi cps_vector_byte",
525                    });
526                }
527                let cps_vector = body[pos..pos + cps_vector_length].to_vec();
528                pos += cps_vector_length;
529                cps_vectors.push(CpsVectorEntry {
530                    c_and_r_regime_mask,
531                    cps_vector,
532                });
533            }
534        }
535
536        Ok(CpcmUsi {
537            copy_control,
538            do_not_cpcm_scramble,
539            viewable,
540            view_window_activated,
541            view_period_activated,
542            simultaneous_view_count_activated,
543            move_local,
544            view_local,
545            move_and_copy_propagation_information,
546            view_propagation_information,
547            remote_access_date_moving_window_flag,
548            remote_access_date_immediate_flag,
549            remote_access_record_flag,
550            export_controlled_cps,
551            export_beyond_trust,
552            disable_analogue_sd_export,
553            disable_analogue_sd_consumption,
554            disable_analogue_hd_export,
555            disable_analogue_hd_consumption,
556            image_constraint,
557            view_window_start,
558            view_window_end,
559            view_period_from_first_playback,
560            simultaneous_view_count,
561            remote_access_date,
562            cps_vectors,
563        })
564    }
565}
566
567impl CpcmUsi {
568    /// Compute the byte length of the body after the `length` field.
569    fn body_len(&self) -> usize {
570        let mut n = USI_FLAGS_LEN;
571        if self.view_window_activated {
572            n += CPCM_DATE_TIME_LEN * 2;
573        }
574        if self.view_period_activated {
575            n += CPCM_PLAYBACK_PERIOD_LEN;
576        }
577        if self.simultaneous_view_count_activated {
578            n += SIMULTANEOUS_VIEW_COUNT_LEN;
579        }
580        if self.remote_access_date_immediate_flag || self.remote_access_date_moving_window_flag {
581            n += CPCM_DATE_TIME_LEN;
582        }
583        if self.export_controlled_cps {
584            n += 1; // cps_vector_count
585            for entry in &self.cps_vectors {
586                n += CPS_ENTRY_HDR_LEN + entry.cps_vector.len();
587            }
588        }
589        n
590    }
591}
592
593impl Serialize for CpcmUsi {
594    type Error = crate::error::Error;
595
596    fn serialized_len(&self) -> usize {
597        1 + self.body_len() // length byte + body
598    }
599
600    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
601        let total = self.serialized_len();
602        if buf.len() < total {
603            return Err(Error::OutputBufferTooSmall {
604                need: total,
605                have: buf.len(),
606            });
607        }
608        let body_len = self.body_len();
609        buf[0] = body_len as u8;
610
611        // ── byte 1 ─────────────────────────────────────────────────────────
612        buf[1] = (self.copy_control.to_u8() << 5)
613            | ((u8::from(self.do_not_cpcm_scramble)) << 4)
614            | ((u8::from(self.viewable)) << 3)
615            | ((u8::from(self.view_window_activated)) << 2)
616            | ((u8::from(self.view_period_activated)) << 1)
617            | u8::from(self.simultaneous_view_count_activated);
618
619        // ── byte 2 ─────────────────────────────────────────────────────────
620        buf[2] = ((u8::from(self.move_local)) << 7)
621            | ((u8::from(self.view_local)) << 6)
622            | ((self.move_and_copy_propagation_information.to_u8() & 0x03) << 4)
623            | ((self.view_propagation_information.to_u8() & 0x03) << 2)
624            | ((u8::from(self.remote_access_date_moving_window_flag)) << 1)
625            | u8::from(self.remote_access_date_immediate_flag);
626
627        // ── byte 3 ─────────────────────────────────────────────────────────
628        buf[3] = ((u8::from(self.remote_access_record_flag)) << 7)
629            | ((u8::from(self.export_controlled_cps)) << 6)
630            | ((u8::from(self.export_beyond_trust)) << 5)
631            | ((u8::from(self.disable_analogue_sd_export)) << 4)
632            | ((u8::from(self.disable_analogue_sd_consumption)) << 3)
633            | ((u8::from(self.disable_analogue_hd_export)) << 2)
634            | ((u8::from(self.disable_analogue_hd_consumption)) << 1)
635            | u8::from(self.image_constraint);
636
637        let mut pos = 1 + USI_FLAGS_LEN; // 4
638
639        // ── conditional fields ──────────────────────────────────────────────
640        if self.view_window_activated {
641            if let (Some(start), Some(end)) = (self.view_window_start, self.view_window_end) {
642                buf[pos..pos + CPCM_DATE_TIME_LEN].copy_from_slice(&start);
643                pos += CPCM_DATE_TIME_LEN;
644                buf[pos..pos + CPCM_DATE_TIME_LEN].copy_from_slice(&end);
645                pos += CPCM_DATE_TIME_LEN;
646            }
647        }
648        if self.view_period_activated {
649            if let Some(vp) = self.view_period_from_first_playback {
650                buf[pos..pos + CPCM_PLAYBACK_PERIOD_LEN].copy_from_slice(&vp);
651                pos += CPCM_PLAYBACK_PERIOD_LEN;
652            }
653        }
654        if self.simultaneous_view_count_activated {
655            if let Some(svc) = self.simultaneous_view_count {
656                buf[pos] = svc;
657                pos += SIMULTANEOUS_VIEW_COUNT_LEN;
658            }
659        }
660        if self.remote_access_date_immediate_flag || self.remote_access_date_moving_window_flag {
661            if let Some(rad) = self.remote_access_date {
662                buf[pos..pos + CPCM_DATE_TIME_LEN].copy_from_slice(&rad);
663                pos += CPCM_DATE_TIME_LEN;
664            }
665        }
666        if self.export_controlled_cps {
667            buf[pos] = self.cps_vectors.len() as u8;
668            pos += 1;
669            for entry in &self.cps_vectors {
670                buf[pos] = entry.c_and_r_regime_mask;
671                let vlen = entry.cps_vector.len() as u16;
672                buf[pos + 1..pos + 3].copy_from_slice(&vlen.to_be_bytes());
673                pos += CPS_ENTRY_HDR_LEN;
674                buf[pos..pos + entry.cps_vector.len()].copy_from_slice(&entry.cps_vector);
675                pos += entry.cps_vector.len();
676            }
677        }
678        Ok(pos)
679    }
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685    use crate::descriptors::extension::test_support::*;
686    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor};
687    use dvb_common::Serialize;
688
689    // ── helper: construct a minimal USI (no conditional fields) ──────────────
690    fn minimal_usi() -> CpcmUsi {
691        CpcmUsi {
692            copy_control: CopyControl::CopyOnce,
693            do_not_cpcm_scramble: false,
694            viewable: true,
695            view_window_activated: false,
696            view_period_activated: false,
697            simultaneous_view_count_activated: false,
698            move_local: false,
699            view_local: false,
700            move_and_copy_propagation_information: MoveCopyPropagation::Mlad,
701            view_propagation_information: ViewPropagation::Vlad,
702            remote_access_date_moving_window_flag: false,
703            remote_access_date_immediate_flag: false,
704            remote_access_record_flag: false,
705            export_controlled_cps: false,
706            export_beyond_trust: false,
707            disable_analogue_sd_export: false,
708            disable_analogue_sd_consumption: false,
709            disable_analogue_hd_export: false,
710            disable_analogue_hd_consumption: false,
711            image_constraint: false,
712            view_window_start: None,
713            view_window_end: None,
714            view_period_from_first_playback: None,
715            simultaneous_view_count: None,
716            remote_access_date: None,
717            cps_vectors: Vec::new(),
718        }
719    }
720
721    fn round_trip_usi(usi: &CpcmUsi) {
722        let mut buf = vec![0u8; usi.serialized_len()];
723        usi.serialize_into(&mut buf).unwrap();
724        let parsed = CpcmUsi::parse(&buf).unwrap();
725        assert_eq!(usi, &parsed, "USI round-trip mismatch");
726    }
727
728    // ─────────────────────────────────────────────────────────────────────────
729    // Byte-anchored minimal USI test.
730    //
731    // For `copy_control=CopyOnce(1)`, `viewable=true`, all else 0:
732    //   byte 0: length = 3 (3 flag bytes follow, no conditional fields)
733    //   byte 1: copy_control[7:5]=001 | do_not=0 | viewable=1 | vwa=0 | vpa=0 | svca=0
734    //           = 0b0010_1000 = 0x28
735    //   byte 2: all zero = 0x00
736    //   byte 3: all zero = 0x00
737    // ─────────────────────────────────────────────────────────────────────────
738    #[test]
739    fn minimal_usi_byte_anchor() {
740        let usi = minimal_usi();
741        let expected = [
742            0x03, // length=3
743            0x28, // copy_control=1<<5=0x20 | viewable=1<<3=0x08 => 0x28
744            0x00, 0x00,
745        ];
746        let mut buf = vec![0u8; usi.serialized_len()];
747        usi.serialize_into(&mut buf).unwrap();
748        assert_eq!(
749            buf.as_slice(),
750            &expected,
751            "byte-anchor mismatch for minimal USI"
752        );
753        let back = CpcmUsi::parse(&expected).unwrap();
754        assert_eq!(usi, back);
755    }
756
757    // ── Conditional: view_window (two 40-bit dates) ───────────────────────────
758    #[test]
759    fn usi_view_window_round_trip() {
760        let mut usi = minimal_usi();
761        usi.view_window_activated = true;
762        usi.view_window_start = Some([0x01, 0x02, 0x03, 0x04, 0x05]);
763        usi.view_window_end = Some([0x06, 0x07, 0x08, 0x09, 0x0A]);
764        round_trip_usi(&usi);
765        // Verify length field: 3 flags + 5 + 5 = 13
766        let mut buf = vec![0u8; usi.serialized_len()];
767        usi.serialize_into(&mut buf).unwrap();
768        assert_eq!(buf[0], 13, "length byte for view_window USI");
769        // view_window_activated bit set in byte 1 bit 2
770        assert_ne!(buf[1] & 0x04, 0, "view_window_activated bit must be set");
771        // view_window_start starts at buf[4]
772        assert_eq!(&buf[4..9], &[0x01, 0x02, 0x03, 0x04, 0x05]);
773        assert_eq!(&buf[9..14], &[0x06, 0x07, 0x08, 0x09, 0x0A]);
774    }
775
776    // ── Conditional: view_period (16-bit) ────────────────────────────────────
777    #[test]
778    fn usi_view_period_round_trip() {
779        let mut usi = minimal_usi();
780        usi.view_period_activated = true;
781        usi.view_period_from_first_playback = Some([0xAB, 0xCD]);
782        round_trip_usi(&usi);
783        let mut buf = vec![0u8; usi.serialized_len()];
784        usi.serialize_into(&mut buf).unwrap();
785        assert_eq!(buf[0], 5, "length byte for view_period USI (3+2)");
786        assert_eq!(&buf[4..6], &[0xAB, 0xCD]);
787    }
788
789    // ── Conditional: simultaneous_view_count (8-bit) ─────────────────────────
790    #[test]
791    fn usi_simultaneous_view_count_round_trip() {
792        let mut usi = minimal_usi();
793        usi.simultaneous_view_count_activated = true;
794        usi.simultaneous_view_count = Some(4);
795        round_trip_usi(&usi);
796        let mut buf = vec![0u8; usi.serialized_len()];
797        usi.serialize_into(&mut buf).unwrap();
798        assert_eq!(buf[0], 4, "length byte for svc USI (3+1)");
799        assert_eq!(buf[4], 4u8);
800    }
801
802    // ── Conditional: remote_access_date (40-bit) ─────────────────────────────
803    #[test]
804    fn usi_remote_access_date_round_trip() {
805        let mut usi = minimal_usi();
806        usi.remote_access_date_immediate_flag = true;
807        usi.remote_access_date = Some([0x11, 0x22, 0x33, 0x44, 0x55]);
808        round_trip_usi(&usi);
809        let mut buf = vec![0u8; usi.serialized_len()];
810        usi.serialize_into(&mut buf).unwrap();
811        assert_eq!(buf[0], 8, "length byte for rad USI (3+5)");
812        assert_eq!(&buf[4..9], &[0x11, 0x22, 0x33, 0x44, 0x55]);
813    }
814
815    // ── Conditional: export_controlled_cps (CPS vector) ─────────────────────
816    #[test]
817    fn usi_export_controlled_cps_round_trip() {
818        // Two CPS entries: first has 2-byte payload, second has 3-byte payload.
819        // Total body: 3 flags + 1 (count) + (1+2+2) + (1+2+3) = 3+1+5+6 = 15
820        let mut usi = minimal_usi();
821        usi.export_controlled_cps = true;
822        usi.cps_vectors = vec![
823            CpsVectorEntry {
824                c_and_r_regime_mask: 0xA5,
825                cps_vector: vec![0x10, 0x20],
826            },
827            CpsVectorEntry {
828                c_and_r_regime_mask: 0x3C,
829                cps_vector: vec![0xDE, 0xAD, 0xBE],
830            },
831        ];
832        round_trip_usi(&usi);
833        let mut buf = vec![0u8; usi.serialized_len()];
834        usi.serialize_into(&mut buf).unwrap();
835        assert_eq!(buf[0], 15, "length byte for cps USI");
836        // cps_vector_count at byte 4
837        assert_eq!(buf[4], 2);
838        // First entry: C_and_R_regime_mask=0xA5, length=0x0002, payload=0x10,0x20
839        assert_eq!(buf[5], 0xA5);
840        assert_eq!(&buf[6..8], &[0x00, 0x02]);
841        assert_eq!(&buf[8..10], &[0x10, 0x20]);
842        // Second entry
843        assert_eq!(buf[10], 0x3C);
844        assert_eq!(&buf[11..13], &[0x00, 0x03]);
845        assert_eq!(&buf[13..16], &[0xDE, 0xAD, 0xBE]);
846    }
847
848    // ── Mutation test ─────────────────────────────────────────────────────────
849    #[test]
850    fn usi_mutation_changes_expected_bytes() {
851        let usi1 = minimal_usi();
852        let mut usi2 = minimal_usi();
853        usi2.copy_control = CopyControl::CopyNeverZeroRetentionAsserted; // 4 = 0b100
854        usi2.image_constraint = true;
855
856        let mut buf1 = vec![0u8; usi1.serialized_len()];
857        let mut buf2 = vec![0u8; usi2.serialized_len()];
858        usi1.serialize_into(&mut buf1).unwrap();
859        usi2.serialize_into(&mut buf2).unwrap();
860
861        // byte 1: copy_control at [7:5]
862        // usi1: 0x28 (CopyOnce=1 → 0x20 | viewable=0x08)
863        // usi2: CopyNeverZeroRetentionAsserted=4 → 0x80 | viewable=0x08 = 0x88
864        assert_eq!(buf1[1], 0x28);
865        assert_eq!(buf2[1], 0x88);
866        // byte 3: image_constraint at bit 0
867        assert_eq!(buf1[3], 0x00);
868        assert_eq!(buf2[3], 0x01);
869    }
870
871    // ── CopyControl value mapping per Table 9 ────────────────────────────────
872    #[test]
873    fn copy_control_table9_mapping() {
874        assert_eq!(CopyControl::from_u8(0), CopyControl::CopyControlNotAsserted);
875        assert_eq!(CopyControl::from_u8(1), CopyControl::CopyOnce);
876        assert_eq!(CopyControl::from_u8(2), CopyControl::CopyNoMore);
877        assert_eq!(
878            CopyControl::from_u8(3),
879            CopyControl::CopyNeverZeroRetentionNotAsserted
880        );
881        assert_eq!(
882            CopyControl::from_u8(4),
883            CopyControl::CopyNeverZeroRetentionAsserted
884        );
885        // 5-7 reserved
886        assert_eq!(CopyControl::from_u8(5), CopyControl::Reserved(5));
887        assert_eq!(CopyControl::from_u8(6), CopyControl::Reserved(6));
888        assert_eq!(CopyControl::from_u8(7), CopyControl::Reserved(7));
889        // round-trip to_u8
890        for v in 0u8..=7 {
891            assert_eq!(CopyControl::from_u8(v).to_u8(), v);
892        }
893        // name() on reserved
894        assert_eq!(CopyControl::Reserved(5).name(), "reserved");
895    }
896
897    // ── usi() returns None for cpcm_version != 1 ─────────────────────────────
898    #[test]
899    fn usi_returns_none_for_non_v1() {
900        let desc = CpcmDeliverySignalling {
901            cpcm_version: 2,
902            selector_bytes: &[0x03, 0x28, 0x00, 0x00],
903        };
904        assert!(desc.usi().is_none());
905
906        let desc0 = CpcmDeliverySignalling {
907            cpcm_version: 0,
908            selector_bytes: &[],
909        };
910        assert!(desc0.usi().is_none());
911    }
912
913    // ── usi() returns Some for cpcm_version == 1 ─────────────────────────────
914    #[test]
915    fn usi_returns_some_for_v1() {
916        let selector = [0x03u8, 0x28, 0x00, 0x00];
917        let desc = CpcmDeliverySignalling {
918            cpcm_version: 1,
919            selector_bytes: &selector,
920        };
921        let usi = desc
922            .usi()
923            .expect("should be Some")
924            .expect("should parse OK");
925        assert_eq!(usi.copy_control, CopyControl::CopyOnce);
926        assert!(usi.viewable);
927    }
928
929    // ── Existing TSDuck round-trip tests (kept passing unchanged) ────────────
930    #[test]
931    fn parse_cpcm_delivery_signalling_structured() {
932        // cpcm_version=1, then 4 selector bytes
933        let sel = [0x01, 0x39, 0x24, 0x45, 0x03];
934        let bytes = wrap(0x01, &sel);
935        let d = ExtensionDescriptor::parse(&bytes).unwrap();
936        match &d.body {
937            ExtensionBody::CpcmDeliverySignalling(b) => {
938                assert_eq!(b.cpcm_version, 1);
939                assert_eq!(b.selector_bytes, &[0x39, 0x24, 0x45, 0x03]);
940            }
941            other => panic!("expected CpcmDeliverySignalling, got {other:?}"),
942        }
943        round_trip(&d);
944    }
945
946    #[test]
947    fn parse_cpcm_delivery_signalling_version_only() {
948        let sel = [0x01]; // cpcm_version, empty selector
949        let bytes = wrap(0x01, &sel);
950        let d = ExtensionDescriptor::parse(&bytes).unwrap();
951        match &d.body {
952            ExtensionBody::CpcmDeliverySignalling(b) => {
953                assert_eq!(b.cpcm_version, 1);
954                assert!(b.selector_bytes.is_empty());
955            }
956            other => panic!("expected CpcmDeliverySignalling, got {other:?}"),
957        }
958        round_trip(&d);
959    }
960}