Skip to main content

dvb_si/descriptors/
scrambling.rs

1//! Scrambling Descriptor — ETSI EN 300 468 §6.2.32 (tag 0x65).
2//!
3//! A single byte identifying the scrambling mode in use (Table 86 syntax /
4//! Table 87 coding, PDF pp. 98-99): 0x01 = DVB-CSA1, 0x02 = DVB-CSA2,
5//! 0x03 = DVB-CSA3, 0x10 = DVB-CISSA v1, etc.
6
7use super::descriptor_body;
8use crate::error::{Error, Result};
9use dvb_common::{Parse, Serialize};
10
11/// Descriptor tag for scrambling_descriptor.
12pub const TAG: u8 = 0x65;
13const HEADER_LEN: usize = 2;
14/// Fixed payload length: a single scrambling_mode byte (EN 300 468 Table 86).
15const BODY_LEN: u8 = 1;
16
17/// Scrambling mode — ETSI EN 300 468 Table 87.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize))]
20#[non_exhaustive]
21pub enum ScramblingMode {
22    /// 0x01 — DVB-CSA1.
23    DvbCsa1,
24    /// 0x02 — DVB-CSA2.
25    DvbCsa2,
26    /// 0x03 — DVB-CSA3 standard.
27    DvbCsa3,
28    /// 0x04 — DVB-CSA3 minimal enhanced.
29    DvbCsa3MinimalEnhanced,
30    /// 0x05 — DVB-CSA3 fully enhanced.
31    DvbCsa3FullyEnhanced,
32    /// 0x10 — DVB-CISSA v1.
33    DvbCissaV1,
34    /// Reserved/unallocated wire value, preserved verbatim for round-trip.
35    Reserved(u8),
36}
37
38impl ScramblingMode {
39    #[must_use]
40    /// Creates a value from a wire byte, preserving every possible
41    /// byte value for lossless round-trip.
42    pub fn from_u8(v: u8) -> Self {
43        match v {
44            0x01 => Self::DvbCsa1,
45            0x02 => Self::DvbCsa2,
46            0x03 => Self::DvbCsa3,
47            0x04 => Self::DvbCsa3MinimalEnhanced,
48            0x05 => Self::DvbCsa3FullyEnhanced,
49            0x10 => Self::DvbCissaV1,
50            v => Self::Reserved(v),
51        }
52    }
53
54    #[must_use]
55    /// Returns the wire byte for this value.
56    pub fn to_u8(self) -> u8 {
57        match self {
58            Self::DvbCsa1 => 0x01,
59            Self::DvbCsa2 => 0x02,
60            Self::DvbCsa3 => 0x03,
61            Self::DvbCsa3MinimalEnhanced => 0x04,
62            Self::DvbCsa3FullyEnhanced => 0x05,
63            Self::DvbCissaV1 => 0x10,
64            Self::Reserved(v) => v,
65        }
66    }
67
68    #[must_use]
69    /// Returns a human-readable spec name for this value.
70    pub fn name(self) -> &'static str {
71        match self {
72            Self::DvbCsa1 => "DVB-CSA1",
73            Self::DvbCsa2 => "DVB-CSA2",
74            Self::DvbCsa3 => "DVB-CSA3 (standard)",
75            Self::DvbCsa3MinimalEnhanced => "DVB-CSA3 (minimal enhanced)",
76            Self::DvbCsa3FullyEnhanced => "DVB-CSA3 (fully enhanced)",
77            Self::DvbCissaV1 => "DVB-CISSA v1",
78            Self::Reserved(_) => "reserved",
79        }
80    }
81}
82
83/// Scrambling Descriptor (tag 0x65).
84#[derive(Debug, Clone, PartialEq, Eq)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize))]
86pub struct ScramblingDescriptor {
87    /// 8-bit scrambling_mode (ETSI Table 87).
88    pub scrambling_mode: ScramblingMode,
89}
90
91impl<'a> Parse<'a> for ScramblingDescriptor {
92    type Error = crate::error::Error;
93    fn parse(bytes: &'a [u8]) -> Result<Self> {
94        let body = descriptor_body(
95            bytes,
96            TAG,
97            "ScramblingDescriptor",
98            "unexpected tag for scrambling_descriptor",
99        )?;
100        if body.len() != BODY_LEN as usize {
101            return Err(Error::InvalidDescriptor {
102                tag: TAG,
103                reason: "scrambling_descriptor length must equal 1",
104            });
105        }
106        Ok(Self {
107            scrambling_mode: ScramblingMode::from_u8(body[0]),
108        })
109    }
110}
111
112impl Serialize for ScramblingDescriptor {
113    type Error = crate::error::Error;
114    fn serialized_len(&self) -> usize {
115        HEADER_LEN + BODY_LEN as usize
116    }
117
118    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
119        let len = self.serialized_len();
120        if buf.len() < len {
121            return Err(Error::OutputBufferTooSmall {
122                need: len,
123                have: buf.len(),
124            });
125        }
126        buf[0] = TAG;
127        buf[1] = BODY_LEN;
128        buf[HEADER_LEN] = self.scrambling_mode.to_u8();
129        Ok(len)
130    }
131}
132impl<'a> crate::traits::DescriptorDef<'a> for ScramblingDescriptor {
133    const TAG: u8 = TAG;
134    const NAME: &'static str = "SCRAMBLING";
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn parse_extracts_scrambling_mode() {
143        let bytes = [TAG, 1, 0x02];
144        let d = ScramblingDescriptor::parse(&bytes).unwrap();
145        assert_eq!(d.scrambling_mode, ScramblingMode::DvbCsa2);
146    }
147
148    #[test]
149    fn parse_rejects_wrong_tag() {
150        let err = ScramblingDescriptor::parse(&[0x66, 1, 0x02]).unwrap_err();
151        assert!(matches!(err, Error::InvalidDescriptor { tag: 0x66, .. }));
152    }
153
154    #[test]
155    fn parse_rejects_short_buffer() {
156        let err = ScramblingDescriptor::parse(&[TAG]).unwrap_err();
157        assert!(matches!(err, Error::BufferTooShort { .. }));
158    }
159
160    #[test]
161    fn parse_rejects_truncated_body() {
162        // length=1 but no payload byte present.
163        let err = ScramblingDescriptor::parse(&[TAG, 1]).unwrap_err();
164        assert!(matches!(err, Error::BufferTooShort { .. }));
165    }
166
167    #[test]
168    fn parse_rejects_wrong_length() {
169        let err = ScramblingDescriptor::parse(&[TAG, 2, 0x02, 0x03]).unwrap_err();
170        assert!(matches!(err, Error::InvalidDescriptor { .. }));
171    }
172
173    #[test]
174    fn serialize_round_trip() {
175        let d = ScramblingDescriptor {
176            scrambling_mode: ScramblingMode::DvbCissaV1,
177        };
178        let mut buf = vec![0u8; d.serialized_len()];
179        d.serialize_into(&mut buf).unwrap();
180        let re = ScramblingDescriptor::parse(&buf).unwrap();
181        assert_eq!(d, re);
182    }
183
184    #[test]
185    fn serialize_rejects_too_small_buffer() {
186        let d = ScramblingDescriptor {
187            scrambling_mode: ScramblingMode::DvbCsa3,
188        };
189        let mut tiny = [0u8; 1];
190        let err = d.serialize_into(&mut tiny).unwrap_err();
191        assert!(matches!(err, Error::OutputBufferTooSmall { .. }));
192    }
193
194    #[test]
195    fn descriptor_length_matches_payload() {
196        let d = ScramblingDescriptor {
197            scrambling_mode: ScramblingMode::DvbCsa1,
198        };
199        assert_eq!(d.serialized_len() - 2, 1);
200    }
201
202    #[cfg(feature = "serde")]
203    #[test]
204    fn serde_round_trip() {
205        let d = ScramblingDescriptor {
206            scrambling_mode: ScramblingMode::DvbCsa2,
207        };
208        let json = serde_json::to_string(&d).unwrap();
209        // Serialize-only: assert the emitted JSON re-parses (serialize-stable).
210        let _v: serde_json::Value = serde_json::from_str(&json).unwrap();
211    }
212
213    #[test]
214    fn scrambling_mode_full_range_round_trip() {
215        for b in 0..=0xFF_u8 {
216            let sm = ScramblingMode::from_u8(b);
217            assert_eq!(sm.to_u8(), b, "round-trip failed for byte 0x{b:02X}");
218        }
219    }
220
221    #[test]
222    fn scrambling_mode_name_for_known() {
223        assert_eq!(ScramblingMode::DvbCsa1.name(), "DVB-CSA1");
224        assert_eq!(ScramblingMode::DvbCissaV1.name(), "DVB-CISSA v1");
225        assert_eq!(ScramblingMode::Reserved(0x55).name(), "reserved");
226    }
227}