Skip to main content

dvb_si/descriptors/extension/
s2xv2_satellite_delivery_system.rs

1//! S2Xv2 Satellite Delivery System Descriptor — ETSI EN 300 468 §6.4.6.5.3
2//! (tag_extension `0x24`).
3//!
4//! Body layout: Table 144a (outer shell) + Table 144b
5//! (`S2Xv2_satellite_delivery_system_info()`).
6//! Mode coding: Table 144c.
7//!
8//! The descriptor has a fixed 18-byte core followed by several conditional
9//! blocks gated on `S2Xv2_mode`, `multiple_input_stream_flag`,
10//! `channel_bond`, and sub-flags. The trailing `for` loop of
11//! `reserved_zero_future_use` bytes is kept verbatim in `reserved_tail`.
12use super::*;
13use crate::descriptors::satellite_delivery_system::{Polarization, RollOff};
14use alloc::vec::Vec;
15
16impl<'a> ExtensionBodyDef<'a> for S2Xv2SatelliteDeliverySystem<'a> {
17    const TAG_EXTENSION: u8 = 0x24;
18    const NAME: &'static str = "S2XV2_SATELLITE_DELIVERY_SYSTEM";
19}
20
21// Fixed-size constants (Table 144b).
22/// Fixed core: delivery_system_id(4) + packed_byte_4(1) + packed_byte_5(1)
23/// + packed_byte_6(1) + satellite_id(3) + frequency(4) + symbol_rate(4) = 18.
24const S2XV2_CORE_LEN: usize = 18;
25/// Scrambling sequence block: reserved(6 bits) + scrambling_sequence_index(18 bits) = 3 bytes.
26const S2XV2_SCRAMBLING_LEN: usize = 3;
27/// Superframe block minimum (no beamhopping): 4 + 3 + 1 = 8 bytes.
28const S2XV2_SUPERFRAME_MIN_LEN: usize = 8;
29/// Superframe block with beamhopping: 8 + 4 = 12 bytes.
30const S2XV2_SUPERFRAME_BH_LEN: usize = 12;
31
32/// S2Xv2 mode — ETSI EN 300 468 Table 144c.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize))]
35#[non_exhaustive]
36pub enum S2Xv2Mode {
37    /// Reserved for future use (0).
38    Reserved0,
39    /// S2X (1).
40    S2X,
41    /// S2X + time slicing (2).
42    S2XTimeSlicing,
43    /// Reserved for future use (3).
44    Reserved3,
45    /// S2X superframe (Annex E of ETSI EN 302 307-2) (4).
46    S2XSuperframe,
47    /// S2X superframe + timeslicing (5).
48    S2XSuperframeTimeSlicing,
49    /// Reserved / future use (6–15).
50    Reserved(u8),
51}
52
53impl S2Xv2Mode {
54    /// Construct from a raw `u8`; every value maps to a variant (total, lossless).
55    #[must_use]
56    pub fn from_u8(v: u8) -> Self {
57        match v {
58            0 => Self::Reserved0,
59            1 => Self::S2X,
60            2 => Self::S2XTimeSlicing,
61            3 => Self::Reserved3,
62            4 => Self::S2XSuperframe,
63            5 => Self::S2XSuperframeTimeSlicing,
64            other => Self::Reserved(other),
65        }
66    }
67
68    /// Inverse of [`from_u8`](Self::from_u8); `Self::Reserved` emits its stored value.
69    #[must_use]
70    pub fn to_u8(self) -> u8 {
71        match self {
72            Self::Reserved0 => 0,
73            Self::S2X => 1,
74            Self::S2XTimeSlicing => 2,
75            Self::Reserved3 => 3,
76            Self::S2XSuperframe => 4,
77            Self::S2XSuperframeTimeSlicing => 5,
78            Self::Reserved(v) => v,
79        }
80    }
81
82    /// Human-readable spec name per Table 144c.
83    #[must_use]
84    pub fn name(self) -> &'static str {
85        match self {
86            Self::Reserved0 => "reserved for future use",
87            Self::S2X => "S2X",
88            Self::S2XTimeSlicing => "S2X + time slicing",
89            Self::Reserved3 => "reserved for future use",
90            Self::S2XSuperframe => "S2X superframe",
91            Self::S2XSuperframeTimeSlicing => "S2X superframe + timeslicing",
92            Self::Reserved(_) => "reserved",
93        }
94    }
95
96    /// True for modes where `scrambling_sequence_selector` / scrambling index apply
97    /// (Table 144b: modes 1 and 2).
98    #[must_use]
99    pub fn has_scrambling_selector(self) -> bool {
100        matches!(self, Self::S2X | Self::S2XTimeSlicing)
101    }
102
103    /// True for modes where `timeslice_number` is present
104    /// (Table 144b: modes 2 and 5).
105    #[must_use]
106    pub fn has_timeslice(self) -> bool {
107        matches!(self, Self::S2XTimeSlicing | Self::S2XSuperframeTimeSlicing)
108    }
109
110    /// True for modes where the superframe block is present
111    /// (Table 144b: modes 4 and 5).
112    #[must_use]
113    pub fn has_superframe(self) -> bool {
114        matches!(self, Self::S2XSuperframe | Self::S2XSuperframeTimeSlicing)
115    }
116}
117dvb_common::impl_spec_display!(S2Xv2Mode, Reserved);
118
119/// Superframe block present in S2Xv2_mode 4 and 5 (Table 144b §6.4.6.5.3).
120#[derive(Debug, Clone, PartialEq, Eq)]
121#[cfg_attr(feature = "serde", derive(serde::Serialize))]
122pub struct S2Xv2Superframe {
123    /// `SOSF_WH_sequence_number` (8 bits).
124    pub sosf_wh_sequence_number: u8,
125    /// `SFFI_selector` (1 bit) — gates `sffi`.
126    pub sffi_selector: bool,
127    /// `beam_hopping_time_plan_selector` (1 bit) — gates `beamhopping_time_plan_id`.
128    pub beam_hopping_time_plan_selector: bool,
129    /// `reference_scrambling_index` (20 bits).
130    pub reference_scrambling_index: u32,
131    /// `SFFI` (4 bits), present iff `sffi_selector == 1`; stored as raw nibble.
132    pub sffi: Option<u8>,
133    /// `payload_scrambling_index` (20 bits).
134    pub payload_scrambling_index: u32,
135    /// `beamhopping_time_plan_id` (32 bits), present iff
136    /// `beam_hopping_time_plan_selector == 1`.
137    pub beamhopping_time_plan_id: Option<u32>,
138    /// `superframe_pilots_WH_sequence_number` (5 bits) — `[7:3]` of the final byte.
139    pub superframe_pilots_wh_sequence_number: u8,
140    /// `postamble_PLI` (3 bits) — `[2:0]` of the final byte.
141    pub postamble_pli: u8,
142}
143
144impl S2Xv2Superframe {
145    /// Wire length in bytes.
146    #[must_use]
147    pub fn serialized_len(&self) -> usize {
148        if self.beam_hopping_time_plan_selector {
149            S2XV2_SUPERFRAME_BH_LEN
150        } else {
151            S2XV2_SUPERFRAME_MIN_LEN
152        }
153    }
154}
155
156/// S2Xv2_satellite_delivery_system body (Tables 144a–144b, §6.4.6.5.3).
157#[derive(Debug, Clone, PartialEq, Eq)]
158#[cfg_attr(feature = "serde", derive(serde::Serialize))]
159#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
160pub struct S2Xv2SatelliteDeliverySystem<'a> {
161    /// `delivery_system_id` (32 bits).
162    pub delivery_system_id: u32,
163    /// `S2Xv2_mode` (4 bits) — Table 144c.
164    pub s2xv2_mode: S2Xv2Mode,
165    /// `multiple_input_stream_flag` (1 bit).
166    pub multiple_input_stream_flag: bool,
167    /// `roll_off` (3 bits) — EN 300 468 Table 144 (same table as S2X).
168    pub roll_off: RollOff,
169    /// `NCR_reference` (1 bit) — Table 144c1.
170    pub ncr_reference: bool,
171    /// `NCR_version` (1 bit).
172    pub ncr_version: bool,
173    /// `channel_bond` (2 bits).
174    pub channel_bond: u8,
175    /// `polarization` (2 bits).
176    pub polarization: Polarization,
177    /// `scrambling_sequence_selector` (1 bit), present (and parsed) iff
178    /// `s2xv2_mode ∈ {1, 2}`; `None` means the bit was `reserved_zero_future_use`.
179    pub scrambling_sequence_selector: Option<bool>,
180    /// `TS_GS_S2X_mode` (2 bits) — Table 143 (raw value).
181    pub ts_gs_s2x_mode: u8,
182    /// `receiver_profiles` (5 bits) — raw bitmask (Table 141 semantics apply).
183    pub receiver_profiles: u8,
184    /// `satellite_id` (24 bits).
185    pub satellite_id: u32,
186    /// `frequency` (32 bits).
187    pub frequency: u32,
188    /// `symbol_rate` (32 bits).
189    pub symbol_rate: u32,
190    /// `input_stream_identifier` (8 bits), present iff `multiple_input_stream_flag`.
191    pub input_stream_identifier: Option<u8>,
192    /// `scrambling_sequence_index` (18 bits), present iff
193    /// `s2xv2_mode ∈ {1, 2}` **and** `scrambling_sequence_selector == 1`.
194    pub scrambling_sequence_index: Option<u32>,
195    /// `timeslice_number` (8 bits), present iff `s2xv2_mode ∈ {2, 5}`.
196    pub timeslice_number: Option<u8>,
197    /// Secondary delivery system IDs from the `channel_bond` loop (one per
198    /// entry, 32 bits each), present iff `channel_bond == 1`.
199    pub secondary_delivery_system_ids: Vec<u32>,
200    /// Superframe block, present iff `s2xv2_mode ∈ {4, 5}`.
201    pub superframe: Option<S2Xv2Superframe>,
202    /// Trailing `reserved_zero_future_use` bytes (verbatim from the closing `for` loop).
203    #[cfg_attr(feature = "serde", serde(borrow))]
204    pub reserved_tail: &'a [u8],
205}
206
207impl<'a> Parse<'a> for S2Xv2SatelliteDeliverySystem<'a> {
208    type Error = crate::error::Error;
209    fn parse(sel: &'a [u8]) -> Result<Self> {
210        if sel.len() < S2XV2_CORE_LEN {
211            return Err(Error::BufferTooShort {
212                need: S2XV2_CORE_LEN,
213                have: sel.len(),
214                what: "S2Xv2 body",
215            });
216        }
217        // Bytes 0–3: delivery_system_id.
218        let (dsid_bytes, _) = sel.split_first_chunk::<4>().ok_or(Error::BufferTooShort {
219            need: S2XV2_CORE_LEN,
220            have: sel.len(),
221            what: "S2Xv2 body",
222        })?;
223        let delivery_system_id = u32::from_be_bytes(*dsid_bytes);
224        // Byte 4: S2Xv2_mode(4) | multiple_input_stream_flag(1) | roll_off(3).
225        let b4 = sel[4];
226        let s2xv2_mode = S2Xv2Mode::from_u8(b4 >> 4);
227        let multiple_input_stream_flag = (b4 & 0x08) != 0;
228        let roll_off = RollOff::from_u8(b4 & 0x07);
229        // Byte 5: reserved(2) | NCR_reference(1) | NCR_version(1) | channel_bond(2) | polarization(2).
230        let b5 = sel[5];
231        let ncr_reference = (b5 & 0x20) != 0;
232        let ncr_version = (b5 & 0x10) != 0;
233        let channel_bond = (b5 >> 2) & 0x03;
234        let polarization = Polarization::from_u8(b5 & 0x03);
235        // Byte 6: scrambling_or_reserved(1) | TS_GS_S2X_mode(2) | receiver_profiles(5).
236        let b6 = sel[6];
237        let raw_scrambling_bit = (b6 >> 7) & 0x01;
238        let ts_gs_s2x_mode = (b6 >> 5) & 0x03;
239        let receiver_profiles = b6 & 0x1F;
240        let scrambling_sequence_selector = if s2xv2_mode.has_scrambling_selector() {
241            Some(raw_scrambling_bit != 0)
242        } else {
243            None
244        };
245        // Bytes 7–9: satellite_id (24 bits, big-endian).
246        let satellite_id = (u32::from(sel[7]) << 16) | (u32::from(sel[8]) << 8) | u32::from(sel[9]);
247        // Bytes 10–13: frequency (32 bits).
248        let (freq_bytes, _) = sel[10..]
249            .split_first_chunk::<4>()
250            .ok_or(Error::BufferTooShort {
251                need: S2XV2_CORE_LEN,
252                have: sel.len(),
253                what: "S2Xv2 body",
254            })?;
255        let frequency = u32::from_be_bytes(*freq_bytes);
256        // Bytes 14–17: symbol_rate (32 bits).
257        let (sr_bytes, _) = sel[14..]
258            .split_first_chunk::<4>()
259            .ok_or(Error::BufferTooShort {
260                need: S2XV2_CORE_LEN,
261                have: sel.len(),
262                what: "S2Xv2 body",
263            })?;
264        let symbol_rate = u32::from_be_bytes(*sr_bytes);
265        let mut pos = S2XV2_CORE_LEN;
266
267        // Conditional: input_stream_identifier.
268        let input_stream_identifier = if multiple_input_stream_flag {
269            if sel.len() < pos + 1 {
270                return Err(Error::BufferTooShort {
271                    need: pos + 1,
272                    have: sel.len(),
273                    what: "S2Xv2 body (input_stream_identifier)",
274                });
275            }
276            let isi = sel[pos];
277            pos += 1;
278            Some(isi)
279        } else {
280            None
281        };
282
283        // Conditional: scrambling_sequence_index (mode 1 or 2, selector == 1).
284        let scrambling_sequence_index = if scrambling_sequence_selector == Some(true) {
285            if sel.len() < pos + S2XV2_SCRAMBLING_LEN {
286                return Err(Error::BufferTooShort {
287                    need: pos + S2XV2_SCRAMBLING_LEN,
288                    have: sel.len(),
289                    what: "S2Xv2 body (scrambling_sequence_index)",
290                });
291            }
292            // reserved(6) | scrambling_sequence_index(18).
293            let idx = (u32::from(sel[pos] & 0x03) << 16)
294                | (u32::from(sel[pos + 1]) << 8)
295                | u32::from(sel[pos + 2]);
296            pos += S2XV2_SCRAMBLING_LEN;
297            Some(idx)
298        } else {
299            None
300        };
301
302        // Conditional: timeslice_number (mode 2 or 5).
303        let timeslice_number = if s2xv2_mode.has_timeslice() {
304            if sel.len() < pos + 1 {
305                return Err(Error::BufferTooShort {
306                    need: pos + 1,
307                    have: sel.len(),
308                    what: "S2Xv2 body (timeslice_number)",
309                });
310            }
311            let ts = sel[pos];
312            pos += 1;
313            Some(ts)
314        } else {
315            None
316        };
317
318        // Conditional: channel_bond loop.
319        let secondary_delivery_system_ids = if channel_bond == 1 {
320            if sel.len() < pos + 1 {
321                return Err(Error::BufferTooShort {
322                    need: pos + 1,
323                    have: sel.len(),
324                    what: "S2Xv2 body (channel_bond header)",
325                });
326            }
327            let bond_byte = sel[pos];
328            pos += 1;
329            // reserved(7) | num_channel_bonds_minus_one(1).
330            let n = (bond_byte & 0x01) as usize + 1;
331            let mut ids = Vec::with_capacity(n);
332            for _ in 0..n {
333                if sel.len() < pos + 4 {
334                    return Err(Error::BufferTooShort {
335                        need: pos + 4,
336                        have: sel.len(),
337                        what: "S2Xv2 body (secondary_delivery_system_id)",
338                    });
339                }
340                let (id_bytes, _) = sel
341                    .get(pos..)
342                    .and_then(|s| s.split_first_chunk::<4>())
343                    .ok_or(Error::BufferTooShort {
344                        need: pos + 4,
345                        have: sel.len(),
346                        what: "S2Xv2 body (secondary_delivery_system_id)",
347                    })?;
348                let id = u32::from_be_bytes(*id_bytes);
349                ids.push(id);
350                pos += 4;
351            }
352            ids
353        } else {
354            Vec::new()
355        };
356
357        // Conditional: superframe block (mode 4 or 5).
358        let superframe = if s2xv2_mode.has_superframe() {
359            if sel.len() < pos + S2XV2_SUPERFRAME_MIN_LEN {
360                return Err(Error::BufferTooShort {
361                    need: pos + S2XV2_SUPERFRAME_MIN_LEN,
362                    have: sel.len(),
363                    what: "S2Xv2 body (superframe)",
364                });
365            }
366            let sosf_wh_sequence_number = sel[pos];
367            // pos+1: SFFI_selector(1) | BH_selector(1) | reserved(2) | ref_scram_hi(4).
368            let b1 = sel[pos + 1];
369            let sffi_selector = (b1 & 0x80) != 0;
370            let beam_hopping_time_plan_selector = (b1 & 0x40) != 0;
371            let ref_scram_hi = u32::from(b1 & 0x0F);
372            let reference_scrambling_index =
373                (ref_scram_hi << 16) | (u32::from(sel[pos + 2]) << 8) | u32::from(sel[pos + 3]);
374            // pos+4: SFFI_or_reserved(4) | psi_hi(4).
375            let b4s = sel[pos + 4];
376            let sffi = if sffi_selector { Some(b4s >> 4) } else { None };
377            let psi_hi = u32::from(b4s & 0x0F);
378            let payload_scrambling_index =
379                (psi_hi << 16) | (u32::from(sel[pos + 5]) << 8) | u32::from(sel[pos + 6]);
380            pos += 7;
381            // Conditional: beamhopping_time_plan_id.
382            let beamhopping_time_plan_id = if beam_hopping_time_plan_selector {
383                if sel.len() < pos + 4 {
384                    return Err(Error::BufferTooShort {
385                        need: pos + 4,
386                        have: sel.len(),
387                        what: "S2Xv2 body (beamhopping_time_plan_id)",
388                    });
389                }
390                let (bh_bytes, _) = sel
391                    .get(pos..)
392                    .and_then(|s| s.split_first_chunk::<4>())
393                    .ok_or(Error::BufferTooShort {
394                        need: pos + 4,
395                        have: sel.len(),
396                        what: "S2Xv2 body (beamhopping_time_plan_id)",
397                    })?;
398                let id = u32::from_be_bytes(*bh_bytes);
399                pos += 4;
400                Some(id)
401            } else {
402                None
403            };
404            // Final superframe byte: pilots(5) | postamble_PLI(3).
405            if sel.len() < pos + 1 {
406                return Err(Error::BufferTooShort {
407                    need: pos + 1,
408                    have: sel.len(),
409                    what: "S2Xv2 body (superframe final byte)",
410                });
411            }
412            let last = sel[pos];
413            let superframe_pilots_wh_sequence_number = (last >> 3) & 0x1F;
414            let postamble_pli = last & 0x07;
415            pos += 1;
416            Some(S2Xv2Superframe {
417                sosf_wh_sequence_number,
418                sffi_selector,
419                beam_hopping_time_plan_selector,
420                reference_scrambling_index,
421                sffi,
422                payload_scrambling_index,
423                beamhopping_time_plan_id,
424                superframe_pilots_wh_sequence_number,
425                postamble_pli,
426            })
427        } else {
428            None
429        };
430
431        Ok(S2Xv2SatelliteDeliverySystem {
432            delivery_system_id,
433            s2xv2_mode,
434            multiple_input_stream_flag,
435            roll_off,
436            ncr_reference,
437            ncr_version,
438            channel_bond,
439            polarization,
440            scrambling_sequence_selector,
441            ts_gs_s2x_mode,
442            receiver_profiles,
443            satellite_id,
444            frequency,
445            symbol_rate,
446            input_stream_identifier,
447            scrambling_sequence_index,
448            timeslice_number,
449            secondary_delivery_system_ids,
450            superframe,
451            reserved_tail: &sel[pos..],
452        })
453    }
454}
455
456impl Serialize for S2Xv2SatelliteDeliverySystem<'_> {
457    type Error = crate::error::Error;
458    fn serialized_len(&self) -> usize {
459        S2XV2_CORE_LEN
460            + usize::from(self.input_stream_identifier.is_some())
461            + if self.scrambling_sequence_selector == Some(true) {
462                S2XV2_SCRAMBLING_LEN
463            } else {
464                0
465            }
466            + usize::from(self.timeslice_number.is_some())
467            + if self.channel_bond == 1 {
468                1 + self.secondary_delivery_system_ids.len() * 4
469            } else {
470                0
471            }
472            + self.superframe.as_ref().map_or(0, |sf| sf.serialized_len())
473            + self.reserved_tail.len()
474    }
475
476    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
477        let len = self.serialized_len();
478        if buf.len() < len {
479            return Err(Error::OutputBufferTooSmall {
480                need: len,
481                have: buf.len(),
482            });
483        }
484        // Bytes 0–3: delivery_system_id.
485        buf[0..4].copy_from_slice(&self.delivery_system_id.to_be_bytes());
486        // Byte 4: S2Xv2_mode(4) | multiple_input_stream_flag(1) | roll_off(3).
487        buf[4] = ((self.s2xv2_mode.to_u8() & 0x0F) << 4)
488            | (u8::from(self.multiple_input_stream_flag) << 3)
489            | (self.roll_off.to_u8() & 0x07);
490        // Byte 5: reserved(2)=0 | NCR_reference(1) | NCR_version(1) | channel_bond(2) | polarization(2).
491        buf[5] = (u8::from(self.ncr_reference) << 5)
492            | (u8::from(self.ncr_version) << 4)
493            | ((self.channel_bond & 0x03) << 2)
494            | (self.polarization.to_u8() & 0x03);
495        // Byte 6: scrambling_or_reserved(1) | TS_GS_S2X_mode(2) | receiver_profiles(5).
496        let scram_bit: u8 = match self.scrambling_sequence_selector {
497            Some(true) => 1,
498            _ => 0,
499        };
500        buf[6] = (scram_bit << 7)
501            | ((self.ts_gs_s2x_mode & 0x03) << 5)
502            | (self.receiver_profiles & 0x1F);
503        // Bytes 7–9: satellite_id (24 bits).
504        buf[7] = (self.satellite_id >> 16) as u8;
505        buf[8] = (self.satellite_id >> 8) as u8;
506        buf[9] = self.satellite_id as u8;
507        // Bytes 10–13: frequency.
508        buf[10..14].copy_from_slice(&self.frequency.to_be_bytes());
509        // Bytes 14–17: symbol_rate.
510        buf[14..18].copy_from_slice(&self.symbol_rate.to_be_bytes());
511        let mut p = S2XV2_CORE_LEN;
512        // Conditional: input_stream_identifier.
513        if let Some(isi) = self.input_stream_identifier {
514            buf[p] = isi;
515            p += 1;
516        }
517        // Conditional: scrambling block.
518        if self.scrambling_sequence_selector == Some(true) {
519            let idx = self.scrambling_sequence_index.unwrap_or(0) & 0x0003_FFFF;
520            buf[p] = (idx >> 16) as u8 & 0x03;
521            buf[p + 1] = (idx >> 8) as u8;
522            buf[p + 2] = idx as u8;
523            p += S2XV2_SCRAMBLING_LEN;
524        }
525        // Conditional: timeslice_number.
526        if let Some(ts) = self.timeslice_number {
527            buf[p] = ts;
528            p += 1;
529        }
530        // Conditional: channel bond loop.
531        if self.channel_bond == 1 {
532            // reserved(7)=0 | num_channel_bonds_minus_one(1).
533            let n = self.secondary_delivery_system_ids.len().saturating_sub(1) as u8 & 0x01;
534            buf[p] = n;
535            p += 1;
536            for &id in &self.secondary_delivery_system_ids {
537                buf[p..p + 4].copy_from_slice(&id.to_be_bytes());
538                p += 4;
539            }
540        }
541        // Conditional: superframe block.
542        if let Some(sf) = &self.superframe {
543            buf[p] = sf.sosf_wh_sequence_number;
544            let ref_hi = (sf.reference_scrambling_index >> 16) as u8 & 0x0F;
545            buf[p + 1] = (u8::from(sf.sffi_selector) << 7)
546                | (u8::from(sf.beam_hopping_time_plan_selector) << 6)
547                | ref_hi;
548            buf[p + 2] = (sf.reference_scrambling_index >> 8) as u8;
549            buf[p + 3] = sf.reference_scrambling_index as u8;
550            let sffi_nibble: u8 = sf.sffi.unwrap_or(0) & 0x0F;
551            let psi_hi = (sf.payload_scrambling_index >> 16) as u8 & 0x0F;
552            buf[p + 4] = (sffi_nibble << 4) | psi_hi;
553            buf[p + 5] = (sf.payload_scrambling_index >> 8) as u8;
554            buf[p + 6] = sf.payload_scrambling_index as u8;
555            p += 7;
556            if let Some(bh_id) = sf.beamhopping_time_plan_id {
557                buf[p..p + 4].copy_from_slice(&bh_id.to_be_bytes());
558                p += 4;
559            }
560            buf[p] =
561                ((sf.superframe_pilots_wh_sequence_number & 0x1F) << 3) | (sf.postamble_pli & 0x07);
562            p += 1;
563        }
564        // Reserved tail.
565        buf[p..p + self.reserved_tail.len()].copy_from_slice(self.reserved_tail);
566        Ok(len)
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573    use crate::descriptors::extension::test_support::*;
574    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor, ExtensionTag};
575
576    /// Build a minimal valid 18-byte selector for S2Xv2 mode 1 (no optional fields).
577    fn minimal_mode1_sel() -> Vec<u8> {
578        let mut sel = Vec::new();
579        // delivery_system_id = 0x0102_0304
580        sel.extend_from_slice(&0x0102_0304u32.to_be_bytes());
581        // b4: S2Xv2_mode=1(0x10), mis=0, roll_off=2(0x02) → 0x12
582        sel.push(0x12);
583        // b5: reserved(2)=0, NCR_reference=1(0x20), NCR_version=0,
584        //     channel_bond=0, polarization=1(LinearVertical) → 0x21
585        sel.push(0x21);
586        // b6: scram_sel=0(mode 1 → uses actual bit), ts_gs=3(0x60), recv_prof=0x07 → 0x67
587        sel.push(0x67);
588        // satellite_id = 0x010203
589        sel.extend_from_slice(&[0x01, 0x02, 0x03]);
590        // frequency = 0x1234_5678
591        sel.extend_from_slice(&0x1234_5678u32.to_be_bytes());
592        // symbol_rate = 0xABCD_EF01
593        sel.extend_from_slice(&0xABCD_EF01u32.to_be_bytes());
594        sel
595    }
596
597    #[test]
598    fn parse_s2xv2_mode1_no_scrambling_round_trip() {
599        // S2Xv2 mode 1, no scrambling, no MIS, no tail.
600        let sel = minimal_mode1_sel();
601        let bytes = wrap(0x24, &sel);
602        let d = ExtensionDescriptor::parse(&bytes).unwrap();
603        assert_eq!(d.kind(), Some(ExtensionTag::S2Xv2SatelliteDeliverySystem));
604        match &d.body {
605            ExtensionBody::S2Xv2SatelliteDeliverySystem(b) => {
606                assert_eq!(b.delivery_system_id, 0x0102_0304);
607                assert_eq!(b.s2xv2_mode, S2Xv2Mode::S2X);
608                assert!(!b.multiple_input_stream_flag);
609                assert_eq!(b.roll_off, RollOff::Alpha020);
610                assert!(b.ncr_reference);
611                assert!(!b.ncr_version);
612                assert_eq!(b.channel_bond, 0);
613                assert_eq!(b.polarization, Polarization::LinearVertical);
614                assert_eq!(b.scrambling_sequence_selector, Some(false));
615                assert_eq!(b.ts_gs_s2x_mode, 3);
616                assert_eq!(b.receiver_profiles, 0x07);
617                assert_eq!(b.satellite_id, 0x010203);
618                assert_eq!(b.frequency, 0x1234_5678);
619                assert_eq!(b.symbol_rate, 0xABCD_EF01);
620                assert!(b.input_stream_identifier.is_none());
621                assert!(b.scrambling_sequence_index.is_none());
622                assert!(b.timeslice_number.is_none());
623                assert!(b.secondary_delivery_system_ids.is_empty());
624                assert!(b.superframe.is_none());
625                assert!(b.reserved_tail.is_empty());
626            }
627            other => panic!("expected S2Xv2, got {other:?}"),
628        }
629        round_trip(&d);
630    }
631
632    #[test]
633    fn parse_s2xv2_mode1_scrambling_and_mis_round_trip() {
634        // S2Xv2 mode 1, scrambling_selector=1, MIS=1, ISI=0x42,
635        // scrambling_sequence_index=0x1ABCD.
636        let mut sel = Vec::new();
637        sel.extend_from_slice(&0xDEAD_BEEFu32.to_be_bytes()); // delivery_system_id
638                                                              // b4: mode=1(0x10), mis=1(0x08), roll_off=1(0x01) → 0x19
639        sel.push(0x19);
640        // b5: NCR_reference=0, NCR_version=1(0x10), channel_bond=0, polarization=3 → 0x13
641        sel.push(0x13);
642        // b6: scram_sel=1(0x80), ts_gs=0, recv_prof=0x1F → 0x9F
643        sel.push(0x9F);
644        sel.extend_from_slice(&[0xAA, 0xBB, 0xCC]); // satellite_id
645        sel.extend_from_slice(&0x0000_0001u32.to_be_bytes()); // frequency
646        sel.extend_from_slice(&0x0000_0002u32.to_be_bytes()); // symbol_rate
647        sel.push(0x42); // input_stream_identifier (MIS=1)
648                        // scrambling block: reserved(6)=0 | scrambling_sequence_index(18) = 0x1ABCD
649                        // 0x1ABCD >> 16 = 0x01 (& 0x03), mid = 0xAB, lo = 0xCD
650        sel.extend_from_slice(&[0x01, 0xAB, 0xCD]);
651        let bytes = wrap(0x24, &sel);
652        let d = ExtensionDescriptor::parse(&bytes).unwrap();
653        match &d.body {
654            ExtensionBody::S2Xv2SatelliteDeliverySystem(b) => {
655                assert_eq!(b.delivery_system_id, 0xDEAD_BEEF);
656                assert_eq!(b.s2xv2_mode, S2Xv2Mode::S2X);
657                assert!(b.multiple_input_stream_flag);
658                // b4 = 0x19: mode=1, mis=1, roll_off=1 → Alpha025
659                assert_eq!(b.roll_off, RollOff::Alpha025);
660                assert!(!b.ncr_reference);
661                assert!(b.ncr_version);
662                assert_eq!(b.channel_bond, 0);
663                assert_eq!(b.polarization, Polarization::CircularRight);
664                assert_eq!(b.scrambling_sequence_selector, Some(true));
665                assert_eq!(b.ts_gs_s2x_mode, 0);
666                assert_eq!(b.receiver_profiles, 0x1F);
667                assert_eq!(b.input_stream_identifier, Some(0x42));
668                assert_eq!(b.scrambling_sequence_index, Some(0x1ABCD));
669                assert!(b.timeslice_number.is_none());
670                assert!(b.secondary_delivery_system_ids.is_empty());
671                assert!(b.superframe.is_none());
672            }
673            other => panic!("expected S2Xv2, got {other:?}"),
674        }
675        round_trip(&d);
676    }
677
678    #[test]
679    fn parse_s2xv2_mode2_timeslice_round_trip() {
680        // S2Xv2 mode 2 (S2X + timeslicing): timeslice_number present, no MIS.
681        let mut sel = Vec::new();
682        sel.extend_from_slice(&0x0000_0001u32.to_be_bytes());
683        // b4: mode=2(0x20), mis=0, roll_off=0 → 0x20
684        sel.push(0x20);
685        sel.push(0x00); // b5
686                        // b6: scram_sel=0, ts_gs=0, recv_prof=1 → 0x01
687        sel.push(0x01);
688        sel.extend_from_slice(&[0, 0, 0]); // satellite_id
689        sel.extend_from_slice(&[0, 0, 0, 0]); // frequency
690        sel.extend_from_slice(&[0, 0, 0, 0]); // symbol_rate
691        sel.push(0x07); // timeslice_number (mode 2 → has_timeslice)
692        let bytes = wrap(0x24, &sel);
693        let d = ExtensionDescriptor::parse(&bytes).unwrap();
694        match &d.body {
695            ExtensionBody::S2Xv2SatelliteDeliverySystem(b) => {
696                assert_eq!(b.s2xv2_mode, S2Xv2Mode::S2XTimeSlicing);
697                assert_eq!(b.timeslice_number, Some(0x07));
698                assert!(b.scrambling_sequence_index.is_none());
699                assert!(b.secondary_delivery_system_ids.is_empty());
700                assert!(b.superframe.is_none());
701            }
702            other => panic!("expected S2Xv2, got {other:?}"),
703        }
704        round_trip(&d);
705    }
706
707    #[test]
708    fn parse_s2xv2_channel_bond_round_trip() {
709        // S2Xv2 mode 0 (reserved), channel_bond=1, two secondary IDs.
710        let mut sel = Vec::new();
711        sel.extend_from_slice(&0xFFFF_FFFFu32.to_be_bytes()); // delivery_system_id
712                                                              // b4: mode=0, mis=0, roll_off=0 → 0x00
713        sel.push(0x00);
714        // b5: channel_bond=1(0x04), polarization=0 → 0x04
715        sel.push(0x04);
716        // b6: all zero
717        sel.push(0x00);
718        sel.extend_from_slice(&[0, 0, 0]); // satellite_id
719        sel.extend_from_slice(&[0, 0, 0, 0]); // frequency
720        sel.extend_from_slice(&[0, 0, 0, 0]); // symbol_rate
721                                              // channel_bond header: reserved(7)=0 | num_channel_bonds_minus_one(1)=1 → N=2
722        sel.push(0x01);
723        sel.extend_from_slice(&0x1111_1111u32.to_be_bytes()); // id[0]
724        sel.extend_from_slice(&0x2222_2222u32.to_be_bytes()); // id[1]
725        let bytes = wrap(0x24, &sel);
726        let d = ExtensionDescriptor::parse(&bytes).unwrap();
727        match &d.body {
728            ExtensionBody::S2Xv2SatelliteDeliverySystem(b) => {
729                assert_eq!(b.channel_bond, 1);
730                assert_eq!(
731                    b.secondary_delivery_system_ids,
732                    vec![0x1111_1111, 0x2222_2222]
733                );
734                assert!(b.superframe.is_none());
735            }
736            other => panic!("expected S2Xv2, got {other:?}"),
737        }
738        round_trip(&d);
739    }
740
741    #[test]
742    fn parse_s2xv2_superframe_no_beamhopping_round_trip() {
743        // S2Xv2 mode 4 (superframe), SFFI_selector=1, no beamhopping.
744        let mut sel = Vec::new();
745        sel.extend_from_slice(&0x0000_0002u32.to_be_bytes()); // delivery_system_id
746                                                              // b4: mode=4(0x40), mis=0, roll_off=0 → 0x40
747        sel.push(0x40);
748        sel.push(0x00); // b5
749                        // b6: no scrambling_selector (mode 4), ts_gs=1(0x20), recv_prof=3(0x03) → 0x23
750        sel.push(0x23);
751        sel.extend_from_slice(&[0, 0, 0]); // satellite_id
752        sel.extend_from_slice(&[0, 0, 0, 0]); // frequency
753        sel.extend_from_slice(&[0, 0, 0, 0]); // symbol_rate
754                                              // Superframe block:
755        sel.push(0xAB); // sosf_wh_sequence_number
756                        // b1: SFFI_selector=1(0x80), BH=0, reserved=0, ref_scram_hi=0x05 → 0x85
757        sel.push(0x85);
758        sel.push(0x12); // ref_scram_mid
759        sel.push(0x34); // ref_scram_lo → ref_scram = 0x051234
760                        // b4: SFFI=0x9(0x90), psi_hi=0x06(0x06) → 0x96
761        sel.push(0x96);
762        sel.push(0x78); // psi_mid
763        sel.push(0x90); // psi_lo → psi = 0x067890
764                        // no beamhopping_time_plan_id
765                        // final: pilots=0x1A(5b)=0b11010, postamble=0x03(3b)=0b011 → 0b11010_011 = 0xD3
766        sel.push(0xD3);
767        let bytes = wrap(0x24, &sel);
768        let d = ExtensionDescriptor::parse(&bytes).unwrap();
769        match &d.body {
770            ExtensionBody::S2Xv2SatelliteDeliverySystem(b) => {
771                assert_eq!(b.s2xv2_mode, S2Xv2Mode::S2XSuperframe);
772                assert!(b.superframe.is_some());
773                let sf = b.superframe.as_ref().unwrap();
774                assert_eq!(sf.sosf_wh_sequence_number, 0xAB);
775                assert!(sf.sffi_selector);
776                assert!(!sf.beam_hopping_time_plan_selector);
777                assert_eq!(sf.reference_scrambling_index, 0x05_1234);
778                assert_eq!(sf.sffi, Some(0x9));
779                assert_eq!(sf.payload_scrambling_index, 0x06_7890);
780                assert!(sf.beamhopping_time_plan_id.is_none());
781                assert_eq!(sf.superframe_pilots_wh_sequence_number, 0x1A);
782                assert_eq!(sf.postamble_pli, 0x03);
783            }
784            other => panic!("expected S2Xv2, got {other:?}"),
785        }
786        round_trip(&d);
787    }
788
789    #[test]
790    fn parse_s2xv2_superframe_with_beamhopping_round_trip() {
791        // S2Xv2 mode 5 (superframe + timeslicing), SFFI=0, beam_hopping=1.
792        let mut sel = Vec::new();
793        sel.extend_from_slice(&0x0000_0003u32.to_be_bytes()); // delivery_system_id
794                                                              // b4: mode=5(0x50), mis=0, roll_off=0 → 0x50
795        sel.push(0x50);
796        sel.push(0x00); // b5
797                        // b6: mode 5 has no scrambling_selector, so scram bit is reserved→0; ts_gs=0, recv_prof=0
798        sel.push(0x00);
799        sel.extend_from_slice(&[0, 0, 0]); // satellite_id
800        sel.extend_from_slice(&[0, 0, 0, 0]); // frequency
801        sel.extend_from_slice(&[0, 0, 0, 0]); // symbol_rate
802                                              // timeslice_number (mode 5 → has_timeslice)
803        sel.push(0x0F);
804        // Superframe block:
805        sel.push(0x01); // sosf_wh_sequence_number
806                        // b1: SFFI=0, BH=1(0x40), reserved=0, ref_scram_hi=0x0A → 0x4A
807        sel.push(0x4A);
808        sel.push(0xBC); // ref_scram_mid
809        sel.push(0xDE); // ref_scram_lo → ref_scram = 0x0ABCDE
810                        // b4: sffi_or_reserved(4)=0, psi_hi=0x00 → 0x00
811        sel.push(0x00);
812        sel.push(0x00); // psi_mid
813        sel.push(0xFF); // psi_lo → psi = 0x0000FF
814                        // beamhopping_time_plan_id = 0x1234_5678
815        sel.extend_from_slice(&0x1234_5678u32.to_be_bytes());
816        // final: pilots=0x05(5b)=0b00101, postamble=0x07(3b)=0b111 → 0b00101_111 = 0x2F
817        sel.push(0x2F);
818        let bytes = wrap(0x24, &sel);
819        let d = ExtensionDescriptor::parse(&bytes).unwrap();
820        match &d.body {
821            ExtensionBody::S2Xv2SatelliteDeliverySystem(b) => {
822                assert_eq!(b.s2xv2_mode, S2Xv2Mode::S2XSuperframeTimeSlicing);
823                assert_eq!(b.timeslice_number, Some(0x0F));
824                let sf = b.superframe.as_ref().unwrap();
825                assert!(!sf.sffi_selector);
826                assert!(sf.beam_hopping_time_plan_selector);
827                assert_eq!(sf.reference_scrambling_index, 0x0A_BCDE);
828                assert!(sf.sffi.is_none());
829                assert_eq!(sf.payload_scrambling_index, 0x0000FF);
830                assert_eq!(sf.beamhopping_time_plan_id, Some(0x1234_5678));
831                assert_eq!(sf.superframe_pilots_wh_sequence_number, 0x05);
832                assert_eq!(sf.postamble_pli, 0x07);
833            }
834            other => panic!("expected S2Xv2, got {other:?}"),
835        }
836        round_trip(&d);
837    }
838
839    #[test]
840    fn parse_s2xv2_reserved_tail_preserved() {
841        // Mode 0 (reserved): no conditionals fire, tail preserved verbatim.
842        let mut sel = Vec::new();
843        sel.extend_from_slice(&0x0000_0000u32.to_be_bytes());
844        // b4: mode=0, mis=0, roll_off=2 → 0x02
845        sel.push(0x02);
846        sel.push(0x00); // b5
847        sel.push(0x00); // b6
848        sel.extend_from_slice(&[0, 0, 0]);
849        sel.extend_from_slice(&[0, 0, 0, 0]);
850        sel.extend_from_slice(&[0, 0, 0, 0]);
851        sel.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); // reserved_tail
852        let bytes = wrap(0x24, &sel);
853        let d = ExtensionDescriptor::parse(&bytes).unwrap();
854        match &d.body {
855            ExtensionBody::S2Xv2SatelliteDeliverySystem(b) => {
856                assert_eq!(b.s2xv2_mode, S2Xv2Mode::Reserved0);
857                assert_eq!(b.reserved_tail, &[0xDE, 0xAD, 0xBE, 0xEF]);
858            }
859            other => panic!("expected S2Xv2, got {other:?}"),
860        }
861        round_trip(&d);
862    }
863
864    #[test]
865    fn parse_s2xv2_rejects_truncated() {
866        // Only 10 bytes — less than S2XV2_CORE_LEN (18).
867        let sel = vec![0u8; 10];
868        let bytes = wrap(0x24, &sel);
869        assert!(matches!(
870            ExtensionDescriptor::parse(&bytes).unwrap_err(),
871            crate::error::Error::BufferTooShort { .. }
872        ));
873    }
874
875    #[test]
876    fn s2xv2_mode_roundtrip_all_values() {
877        for b in 0..=0x0Fu8 {
878            assert_eq!(S2Xv2Mode::from_u8(b).to_u8(), b);
879        }
880    }
881
882    #[cfg(feature = "serde")]
883    #[test]
884    fn serde_serialize_s2xv2() {
885        let sel = minimal_mode1_sel();
886        let bytes = wrap(0x24, &sel);
887        let d = ExtensionDescriptor::parse(&bytes).unwrap();
888        let json = serde_json::to_string(&d).unwrap();
889        // tag_extension = 0x24 = 36
890        assert!(json.contains("\"tag_extension\":36"));
891        assert!(json.contains("\"s2Xv2SatelliteDeliverySystem\""));
892    }
893}