Skip to main content

dvb_si/descriptors/extension/
s2x_satellite_delivery_system.rs

1//! S2X Satellite Delivery System Descriptor — ETSI EN 300 468 §6.4.6.5.2 (tag_extension 0x17).
2//!
3//! The `reserved_tail` field holds trailing `reserved_future_use` bytes
4//! verbatim; future spec growth is surfaced via additive typed accessors.
5use super::*;
6use crate::descriptors::satellite_delivery_system::{Polarization, RollOff};
7use alloc::vec::Vec;
8
9impl<'a> ExtensionBodyDef<'a> for S2XSatelliteDeliverySystem<'a> {
10    const TAG_EXTENSION: u8 = 0x17;
11    const NAME: &'static str = "S2X_SATELLITE_DELIVERY_SYSTEM";
12}
13
14// ---------------------------------------------------------------------------
15//  S2X-specific enums (Tables 141-144)
16// ---------------------------------------------------------------------------
17
18/// S2X mode — ETSI EN 300 468 Table 142.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize))]
21#[non_exhaustive]
22pub enum S2XMode {
23    /// Reserved for future use.
24    Reserved0,
25    /// S2X.
26    S2X,
27    /// S2X + time slicing.
28    S2XTimeSlicing,
29    /// S2X + channel bonding.
30    S2XChannelBonding,
31    /// Reserved / future use.
32    Reserved(u8),
33}
34
35impl S2XMode {
36    #[must_use]
37    /// Construct from a raw `u8`; every value maps to a variant (total, lossless).
38    pub fn from_u8(v: u8) -> Self {
39        match v {
40            0 => S2XMode::Reserved0,
41            1 => S2XMode::S2X,
42            2 => S2XMode::S2XTimeSlicing,
43            3 => S2XMode::S2XChannelBonding,
44            other => S2XMode::Reserved(other),
45        }
46    }
47
48    #[must_use]
49    /// Inverse of `from_u8`; `Self::Reserved` emits its stored value.
50    pub fn to_u8(self) -> u8 {
51        match self {
52            S2XMode::Reserved0 => 0,
53            S2XMode::S2X => 1,
54            S2XMode::S2XTimeSlicing => 2,
55            S2XMode::S2XChannelBonding => 3,
56            S2XMode::Reserved(v) => v,
57        }
58    }
59
60    #[must_use]
61    /// Human-readable spec name per the governing Table.
62    pub fn name(self) -> &'static str {
63        match self {
64            S2XMode::Reserved0 => "reserved for future use",
65            S2XMode::S2X => "S2X",
66            S2XMode::S2XTimeSlicing => "S2X + time slicing",
67            S2XMode::S2XChannelBonding => "S2X + channel bonding",
68            S2XMode::Reserved(_) => "reserved",
69        }
70    }
71}
72dvb_common::impl_spec_display!(S2XMode, Reserved);
73
74/// TS/GS S2X mode — ETSI EN 300 468 Table 143.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize))]
77#[non_exhaustive]
78pub enum TsGsS2XMode {
79    /// Generic packetized.
80    GenericPacketized,
81    /// GSE.
82    Gse,
83    /// GSE high efficiency mode.
84    GseHighEfficiency,
85    /// DVB transport stream.
86    DvbTransportStream,
87    /// Reserved / future use.
88    Reserved(u8),
89}
90
91impl TsGsS2XMode {
92    #[must_use]
93    /// Construct from a raw `u8`; every value maps to a variant (total, lossless).
94    pub fn from_u8(v: u8) -> Self {
95        match v {
96            0 => TsGsS2XMode::GenericPacketized,
97            1 => TsGsS2XMode::Gse,
98            2 => TsGsS2XMode::GseHighEfficiency,
99            3 => TsGsS2XMode::DvbTransportStream,
100            other => TsGsS2XMode::Reserved(other),
101        }
102    }
103
104    #[must_use]
105    /// Inverse of `from_u8`; `Self::Reserved` emits its stored value.
106    pub fn to_u8(self) -> u8 {
107        match self {
108            TsGsS2XMode::GenericPacketized => 0,
109            TsGsS2XMode::Gse => 1,
110            TsGsS2XMode::GseHighEfficiency => 2,
111            TsGsS2XMode::DvbTransportStream => 3,
112            TsGsS2XMode::Reserved(v) => v,
113        }
114    }
115
116    #[must_use]
117    /// Human-readable spec name per the governing Table.
118    pub fn name(self) -> &'static str {
119        match self {
120            TsGsS2XMode::GenericPacketized => "generic packetized",
121            TsGsS2XMode::Gse => "GSE",
122            TsGsS2XMode::GseHighEfficiency => "GSE high efficiency mode",
123            TsGsS2XMode::DvbTransportStream => "DVB transport stream",
124            TsGsS2XMode::Reserved(_) => "reserved",
125        }
126    }
127}
128dvb_common::impl_spec_display!(TsGsS2XMode, Reserved);
129
130// Receiver profile bit flags (additive — field stays raw u8).
131const RP_BROADCAST: u8 = 0x01;
132const RP_INTERACTIVE: u8 = 0x02;
133const RP_DSNG: u8 = 0x04;
134const RP_PROFESSIONAL: u8 = 0x08;
135const RP_VL_SNR: u8 = 0x10;
136
137/// A single channel-bond entry (Table 140 inner `for` loop).
138///
139/// Layout mirrors the primary channel: frequency(4) + orbital_position(2) +
140/// packed byte + symbol_rate(4) + optional input_stream_identifier(1).
141#[derive(Debug, Clone, PartialEq, Eq)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize))]
143pub struct S2XChannelBond {
144    /// frequency(32) — 32-bit BCD (10 kHz resolution, §6.2.13.2).
145    pub frequency: u32,
146    /// orbital_position(16) — 16-bit BCD (tenths of degree).
147    pub orbital_position: u16,
148    /// west_east_flag(1).
149    pub west_east_flag: bool,
150    /// polarization(2).
151    pub polarization: Polarization,
152    /// bonded_channel_multiple_input_stream_flag(1).
153    pub multiple_input_stream_flag: bool,
154    /// roll_off(3) — Table 144.
155    pub roll_off: RollOff,
156    /// symbol_rate(28) — 28-bit BCD (100 sym/s resolution).
157    pub symbol_rate: u32,
158    /// input_stream_identifier(8), present iff `multiple_input_stream_flag`.
159    pub input_stream_identifier: Option<u8>,
160}
161
162impl S2XChannelBond {
163    /// Decode the 32-bit BCD `frequency` to Hz (10 kHz field resolution,
164    /// EN 300 468 §6.2.13.2).
165    #[must_use]
166    pub fn frequency_hz(&self) -> Option<u64> {
167        dvb_common::bcd::bcd_to_decimal(u64::from(self.frequency), 8).map(|v| v * 10_000)
168    }
169
170    /// Decode the 16-bit BCD `orbital_position` to degrees (tenths resolution).
171    #[must_use]
172    pub fn orbital_position_deg(&self) -> Option<f64> {
173        dvb_common::bcd::bcd_to_decimal(u64::from(self.orbital_position), 4)
174            .map(|tenths| tenths as f64 / 10.0)
175    }
176
177    /// Decode the 28-bit BCD `symbol_rate` to symbols/second (100 sym/s resolution).
178    #[must_use]
179    pub fn symbol_rate_sps(&self) -> Option<u64> {
180        dvb_common::bcd::bcd_to_decimal(u64::from(self.symbol_rate), 7).map(|v| v * 100)
181    }
182}
183
184/// S2X_satellite_delivery_system body (Table 140).
185#[derive(Debug, Clone, PartialEq, Eq)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize))]
187#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
188pub struct S2XSatelliteDeliverySystem<'a> {
189    /// receiver_profiles(5) — Table 141 (raw bitmask, see flag accessors below).
190    pub receiver_profiles: u8,
191    /// S2X_mode(2) — Table 142.
192    pub s2x_mode: S2XMode,
193    /// scrambling_sequence_selector(1).
194    pub scrambling_sequence_selector: bool,
195    /// TS_GS_S2X_mode(2) — Table 143.
196    pub ts_gs_s2x_mode: TsGsS2XMode,
197    /// scrambling_sequence_index(18), present iff `scrambling_sequence_selector`.
198    pub scrambling_sequence_index: Option<u32>,
199    /// frequency(32) — primary channel, 32-bit BCD (10 kHz resolution, §6.2.13.2).
200    pub frequency: u32,
201    /// orbital_position(16).
202    pub orbital_position: u16,
203    /// west_east_flag(1).
204    pub west_east_flag: bool,
205    /// polarization(2).
206    pub polarization: Polarization,
207    /// multiple_input_stream_flag(1).
208    pub multiple_input_stream_flag: bool,
209    /// roll_off(3) — Table 144.
210    pub roll_off: RollOff,
211    /// symbol_rate(28) — 28-bit BCD (100 sym/s resolution).
212    pub symbol_rate: u32,
213    /// input_stream_identifier(8), present iff `multiple_input_stream_flag`.
214    pub input_stream_identifier: Option<u8>,
215    /// timeslice_number(8), present iff `s2x_mode == 2`.
216    pub timeslice_number: Option<u8>,
217    /// S2X_mode==3 channel-bond entries (empty unless s2x_mode==3).
218    pub channel_bonds: Vec<S2XChannelBond>,
219    /// Trailing reserved_future_use bytes (opaque), preserved verbatim.
220    pub reserved_tail: &'a [u8],
221}
222
223impl S2XSatelliteDeliverySystem<'_> {
224    /// Decode the 32-bit BCD `frequency` to Hz (10 kHz field resolution,
225    /// EN 300 468 §6.2.13.2).
226    #[must_use]
227    pub fn frequency_hz(&self) -> Option<u64> {
228        dvb_common::bcd::bcd_to_decimal(u64::from(self.frequency), 8).map(|v| v * 10_000)
229    }
230
231    /// Decode the 16-bit BCD `orbital_position` to degrees (tenths resolution).
232    #[must_use]
233    pub fn orbital_position_deg(&self) -> Option<f64> {
234        dvb_common::bcd::bcd_to_decimal(u64::from(self.orbital_position), 4)
235            .map(|tenths| tenths as f64 / 10.0)
236    }
237
238    /// Decode the 28-bit BCD `symbol_rate` to symbols/second (100 sym/s resolution).
239    #[must_use]
240    pub fn symbol_rate_sps(&self) -> Option<u64> {
241        dvb_common::bcd::bcd_to_decimal(u64::from(self.symbol_rate), 7).map(|v| v * 100)
242    }
243
244    // ---- Receiver profile flag accessors (additive, Table 141) ----
245
246    /// Broadcast services (receiver_profiles bit 0).
247    #[must_use]
248    pub fn receiver_broadcast(&self) -> bool {
249        (self.receiver_profiles & RP_BROADCAST) != 0
250    }
251
252    /// Interactive services (receiver_profiles bit 1).
253    #[must_use]
254    pub fn receiver_interactive(&self) -> bool {
255        (self.receiver_profiles & RP_INTERACTIVE) != 0
256    }
257
258    /// DSNG services (receiver_profiles bit 2).
259    #[must_use]
260    pub fn receiver_dsng(&self) -> bool {
261        (self.receiver_profiles & RP_DSNG) != 0
262    }
263
264    /// Professional services (receiver_profiles bit 3).
265    #[must_use]
266    pub fn receiver_professional(&self) -> bool {
267        (self.receiver_profiles & RP_PROFESSIONAL) != 0
268    }
269
270    /// VL-SNR services (receiver_profiles bit 4).
271    #[must_use]
272    pub fn receiver_vl_snr(&self) -> bool {
273        (self.receiver_profiles & RP_VL_SNR) != 0
274    }
275}
276
277const BOND_BASE_LEN: usize = S2X_PRIMARY_LEN;
278
279fn parse_channel_common(
280    sel: &[u8],
281    pos: &mut usize,
282) -> Result<(u32, u16, bool, Polarization, bool, RollOff, u32)> {
283    if sel.len() < *pos + BOND_BASE_LEN {
284        return Err(Error::BufferTooShort {
285            need: *pos + BOND_BASE_LEN,
286            have: sel.len(),
287            what: "S2X body",
288        });
289    }
290    let frequency = u32::from_be_bytes([sel[*pos], sel[*pos + 1], sel[*pos + 2], sel[*pos + 3]]);
291    let orbital_position = u16::from_be_bytes([sel[*pos + 4], sel[*pos + 5]]);
292    let pb = sel[*pos + 6];
293    let west_east_flag = (pb & 0x80) != 0;
294    let polarization = Polarization::from_u8((pb >> 5) & 0x03);
295    let multiple_input_stream_flag = (pb & 0x10) != 0;
296    let roll_off = RollOff::from_u8(pb & 0x07);
297    let symbol_rate = (u32::from(sel[*pos + 7] & 0x0F) << 24)
298        | (u32::from(sel[*pos + 8]) << 16)
299        | (u32::from(sel[*pos + 9]) << 8)
300        | u32::from(sel[*pos + 10]);
301    *pos += BOND_BASE_LEN;
302    Ok((
303        frequency,
304        orbital_position,
305        west_east_flag,
306        polarization,
307        multiple_input_stream_flag,
308        roll_off,
309        symbol_rate,
310    ))
311}
312
313fn write_channel_common(
314    buf: &mut [u8],
315    p: &mut usize,
316    frequency: u32,
317    orbital_position: u16,
318    packed: u8,
319    symbol_rate: u32,
320) {
321    buf[*p..*p + 4].copy_from_slice(&frequency.to_be_bytes());
322    buf[*p + 4..*p + 6].copy_from_slice(&orbital_position.to_be_bytes());
323    buf[*p + 6] = packed;
324    let sr = symbol_rate & 0x0FFF_FFFF;
325    buf[*p + 7] = (sr >> 24) as u8 & 0x0F;
326    buf[*p + 8] = (sr >> 16) as u8;
327    buf[*p + 9] = (sr >> 8) as u8;
328    buf[*p + 10] = sr as u8;
329    *p += BOND_BASE_LEN;
330}
331
332fn pack_we_pol_mis_ro(we: bool, pol: Polarization, mis: bool, ro: RollOff) -> u8 {
333    (u8::from(we) << 7) | ((pol.to_u8() & 0x03) << 5) | (u8::from(mis) << 4) | (ro.to_u8() & 0x07)
334}
335
336impl<'a> Parse<'a> for S2XSatelliteDeliverySystem<'a> {
337    type Error = crate::error::Error;
338    fn parse(sel: &'a [u8]) -> Result<Self> {
339        // receiver_profiles byte + S2X mode/flags byte = 2 fixed bytes.
340        if sel.len() < 2 {
341            return Err(Error::BufferTooShort {
342                need: 2,
343                have: sel.len(),
344                what: "S2X body",
345            });
346        }
347        let receiver_profiles = sel[0] >> 3;
348        let b1 = sel[1];
349        // Table 140 byte 1, MSB-first: S2X_mode(2) scrambling_sequence_selector(1)
350        // reserved_zero_future_use(3) TS_GS_S2X_mode(2).
351        let s2x_mode = S2XMode::from_u8((b1 >> 6) & 0x03);
352        let scrambling_sequence_selector = (b1 & 0x20) != 0;
353        let ts_gs_s2x_mode = TsGsS2XMode::from_u8(b1 & 0x03);
354        let mut pos = 2;
355        let scrambling_sequence_index = if scrambling_sequence_selector {
356            if sel.len() < pos + S2X_SCRAMBLING_LEN {
357                return Err(Error::BufferTooShort {
358                    need: pos + S2X_SCRAMBLING_LEN,
359                    have: sel.len(),
360                    what: "S2X body",
361                });
362            }
363            let idx = (u32::from(sel[pos] & 0x03) << 16)
364                | (u32::from(sel[pos + 1]) << 8)
365                | u32::from(sel[pos + 2]);
366            pos += S2X_SCRAMBLING_LEN;
367            Some(idx)
368        } else {
369            None
370        };
371        // Primary channel (Table 140): frequency(32) orbital_position(16)
372        //   packed byte = west_east(1) polarization(2) mis(1) reserved(1) roll_off(3)
373        //   then reserved(4) | symbol_rate[27:24], and 3 bytes symbol_rate[23:0].
374        let (
375            frequency,
376            orbital_position,
377            west_east_flag,
378            polarization,
379            multiple_input_stream_flag,
380            roll_off,
381            symbol_rate,
382        ) = parse_channel_common(sel, &mut pos)?;
383        let input_stream_identifier = if multiple_input_stream_flag {
384            if sel.len() < pos + 1 {
385                return Err(Error::BufferTooShort {
386                    need: pos + 1,
387                    have: sel.len(),
388                    what: "S2X body",
389                });
390            }
391            let isi = sel[pos];
392            pos += 1;
393            Some(isi)
394        } else {
395            None
396        };
397        let timeslice_number = if s2x_mode == S2XMode::S2XTimeSlicing {
398            if sel.len() < pos + 1 {
399                return Err(Error::BufferTooShort {
400                    need: pos + 1,
401                    have: sel.len(),
402                    what: "S2X body",
403                });
404            }
405            let ts = sel[pos];
406            pos += 1;
407            Some(ts)
408        } else {
409            None
410        };
411        let (channel_bonds, reserved_tail) = if s2x_mode == S2XMode::S2XChannelBonding {
412            // --- channel bonding loop (Table 140) ---
413            if sel.len() < pos + 1 {
414                return Err(Error::BufferTooShort {
415                    need: pos + 1,
416                    have: sel.len(),
417                    what: "S2X body",
418                });
419            }
420            let bond_byte = sel[pos];
421            pos += 1;
422            // reserved_zero_future_use(7) | num_channel_bonds_minus_one(1)
423            let num_channel_bonds = (bond_byte & 0x01) as usize + 1;
424            let mut bonds = Vec::with_capacity(num_channel_bonds);
425            for _ in 0..num_channel_bonds {
426                let (freq, orb, we, pol, mis, ro, sr) = parse_channel_common(sel, &mut pos)?;
427                let isi = if mis {
428                    if sel.len() < pos + 1 {
429                        return Err(Error::BufferTooShort {
430                            need: pos + 1,
431                            have: sel.len(),
432                            what: "S2X body",
433                        });
434                    }
435                    let v = sel[pos];
436                    pos += 1;
437                    Some(v)
438                } else {
439                    None
440                };
441                bonds.push(S2XChannelBond {
442                    frequency: freq,
443                    orbital_position: orb,
444                    west_east_flag: we,
445                    polarization: pol,
446                    multiple_input_stream_flag: mis,
447                    roll_off: ro,
448                    symbol_rate: sr,
449                    input_stream_identifier: isi,
450                });
451            }
452            (bonds, &sel[pos..])
453        } else {
454            (Vec::new(), &sel[pos..])
455        };
456        Ok(S2XSatelliteDeliverySystem {
457            receiver_profiles,
458            s2x_mode,
459            scrambling_sequence_selector,
460            ts_gs_s2x_mode,
461            scrambling_sequence_index,
462            frequency,
463            orbital_position,
464            west_east_flag,
465            polarization,
466            multiple_input_stream_flag,
467            roll_off,
468            symbol_rate,
469            input_stream_identifier,
470            timeslice_number,
471            channel_bonds,
472            reserved_tail,
473        })
474    }
475}
476
477impl Serialize for S2XSatelliteDeliverySystem<'_> {
478    type Error = crate::error::Error;
479    fn serialized_len(&self) -> usize {
480        let bond_len: usize = if self.s2x_mode == S2XMode::S2XChannelBonding {
481            1 + self
482                .channel_bonds
483                .iter()
484                .map(|b| BOND_BASE_LEN + usize::from(b.input_stream_identifier.is_some()))
485                .sum::<usize>()
486        } else {
487            0
488        };
489        2 + if self.scrambling_sequence_selector {
490            S2X_SCRAMBLING_LEN
491        } else {
492            0
493        } + S2X_PRIMARY_LEN
494            + usize::from(self.input_stream_identifier.is_some())
495            + usize::from(self.timeslice_number.is_some())
496            + bond_len
497            + self.reserved_tail.len()
498    }
499    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
500        let len = self.serialized_len();
501        if buf.len() < len {
502            return Err(Error::OutputBufferTooSmall {
503                need: len,
504                have: buf.len(),
505            });
506        }
507        buf[0] = self.receiver_profiles << 3;
508        buf[1] = ((self.s2x_mode.to_u8() & 0x03) << 6)
509            | (u8::from(self.scrambling_sequence_selector) << 5)
510            | (self.ts_gs_s2x_mode.to_u8() & 0x03);
511        let mut p = 2;
512        if self.scrambling_sequence_selector {
513            let idx = self.scrambling_sequence_index.unwrap_or(0) & 0x3FFFF;
514            buf[p] = (idx >> 16) as u8 & 0x03;
515            buf[p + 1] = (idx >> 8) as u8;
516            buf[p + 2] = idx as u8;
517            p += S2X_SCRAMBLING_LEN;
518        }
519        write_channel_common(
520            buf,
521            &mut p,
522            self.frequency,
523            self.orbital_position,
524            pack_we_pol_mis_ro(
525                self.west_east_flag,
526                self.polarization,
527                self.multiple_input_stream_flag,
528                self.roll_off,
529            ),
530            self.symbol_rate,
531        );
532        if let Some(isi) = self.input_stream_identifier {
533            buf[p] = isi;
534            p += 1;
535        }
536        if let Some(ts) = self.timeslice_number {
537            buf[p] = ts;
538            p += 1;
539        }
540        if self.s2x_mode == S2XMode::S2XChannelBonding {
541            // reserved_zero_future_use(7) | num_channel_bonds_minus_one(1)
542            buf[p] = (self.channel_bonds.len() as u8).saturating_sub(1) & 0x01;
543            p += 1;
544            for bond in &self.channel_bonds {
545                write_channel_common(
546                    buf,
547                    &mut p,
548                    bond.frequency,
549                    bond.orbital_position,
550                    pack_we_pol_mis_ro(
551                        bond.west_east_flag,
552                        bond.polarization,
553                        bond.multiple_input_stream_flag,
554                        bond.roll_off,
555                    ),
556                    bond.symbol_rate,
557                );
558                if let Some(isi) = bond.input_stream_identifier {
559                    buf[p] = isi;
560                    p += 1;
561                }
562            }
563        }
564        buf[p..p + self.reserved_tail.len()].copy_from_slice(self.reserved_tail);
565        Ok(len)
566    }
567}
568
569#[cfg(test)]
570mod tests {
571    use super::*;
572    use crate::descriptors::extension::test_support::*;
573    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor};
574
575    #[test]
576    fn s2x_mode_roundtrip() {
577        for b in 0..=0xFFu8 {
578            assert_eq!(S2XMode::from_u8(b).to_u8(), b);
579        }
580    }
581
582    #[test]
583    fn ts_gs_s2x_mode_roundtrip() {
584        for b in 0..=0xFFu8 {
585            assert_eq!(TsGsS2XMode::from_u8(b).to_u8(), b);
586        }
587    }
588
589    #[test]
590    fn parse_s2x_primary_with_isi_and_timeslice() {
591        // receiver_profiles=0x05; s2x_mode=2, scram_sel=0, ts_gs=1; ISI + timeslice
592        let b0 = 0x05 << 3;
593        let b1 = (0x02 << 6) | 0x01; // mode 2 [7:6], no scrambling, ts_gs 1 [1:0]
594        let mut sel = vec![b0, b1];
595        sel.extend_from_slice(&0x0102_0304u32.to_be_bytes()); // frequency
596        sel.extend_from_slice(&0x00C8u16.to_be_bytes()); // orbital_position
597        sel.push((1 << 7) | (0x02 << 5) | (1 << 4) | 0x03); // we=1 pol=2 mis=1 roll=3
598        let sr: u32 = 0x0AB_CDEF; // symbol_rate (28-bit)
599        sel.push((sr >> 24) as u8 & 0x0F);
600        sel.push((sr >> 16) as u8);
601        sel.push((sr >> 8) as u8);
602        sel.push(sr as u8);
603        sel.push(0x42); // input_stream_identifier (mis=1)
604        sel.push(0x09); // timeslice_number (mode==2)
605        let bytes = wrap(0x17, &sel);
606        let d = ExtensionDescriptor::parse(&bytes).unwrap();
607        match &d.body {
608            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
609                assert_eq!(b.receiver_profiles, 0x05);
610                assert_eq!(b.s2x_mode, S2XMode::S2XTimeSlicing);
611                assert!(!b.scrambling_sequence_selector);
612                assert_eq!(b.ts_gs_s2x_mode, TsGsS2XMode::Gse);
613                assert_eq!(b.frequency, 0x0102_0304);
614                assert_eq!(b.orbital_position, 0x00C8);
615                assert!(b.west_east_flag);
616                assert_eq!(b.polarization, Polarization::CircularLeft);
617                assert!(b.multiple_input_stream_flag);
618                assert_eq!(b.roll_off, RollOff::Reserved(3));
619                assert_eq!(b.symbol_rate, 0x0AB_CDEF);
620                assert_eq!(b.input_stream_identifier, Some(0x42));
621                assert_eq!(b.timeslice_number, Some(0x09));
622                assert!(b.channel_bonds.is_empty());
623                assert!(b.reserved_tail.is_empty());
624            }
625            other => panic!("expected S2X, got {other:?}"),
626        }
627        round_trip(&d);
628    }
629
630    #[test]
631    fn parse_s2x_with_scrambling_index() {
632        let b0 = 0x01 << 3;
633        let b1 = (0x01 << 6) | 0x20; // mode 1 [7:6], scrambling selector set [5]
634        let mut sel = vec![b0, b1];
635        // scrambling index 0x2ABCD (18-bit)
636        sel.push(0x02);
637        sel.push(0xAB);
638        sel.push(0xCD);
639        sel.extend_from_slice(&0u32.to_be_bytes()); // frequency
640        sel.extend_from_slice(&0u16.to_be_bytes()); // orbital
641        sel.push(0x00); // packed (mis=0)
642        sel.extend_from_slice(&[0, 0, 0, 0]); // symbol_rate
643        let bytes = wrap(0x17, &sel);
644        let d = ExtensionDescriptor::parse(&bytes).unwrap();
645        match &d.body {
646            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
647                assert!(b.scrambling_sequence_selector);
648                assert_eq!(b.scrambling_sequence_index, Some(0x2ABCD));
649                assert_eq!(b.input_stream_identifier, None);
650                assert_eq!(b.timeslice_number, None);
651                assert!(b.channel_bonds.is_empty());
652                assert!(b.reserved_tail.is_empty());
653            }
654            other => panic!("expected S2X, got {other:?}"),
655        }
656        round_trip(&d);
657    }
658
659    #[test]
660    fn parse_s2x_mode1_tail_preserved() {
661        // mode 1 — no channel bonds; trailing bytes become reserved_tail.
662        let b0 = 0x01 << 3;
663        let b1 = 0x01 << 6; // mode 1 [7:6], no scrambling, ts_gs 0
664        let mut sel = vec![b0, b1];
665        sel.extend_from_slice(&0u32.to_be_bytes());
666        sel.extend_from_slice(&0u16.to_be_bytes());
667        sel.push(0x00); // mis=0
668        sel.extend_from_slice(&[0, 0, 0, 0]); // symbol_rate
669        sel.extend_from_slice(&[0xAA, 0xBB, 0xCC]); // reserved_future_use tail
670        let bytes = wrap(0x17, &sel);
671        let d = ExtensionDescriptor::parse(&bytes).unwrap();
672        match &d.body {
673            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
674                assert_eq!(b.s2x_mode, S2XMode::S2X);
675                assert_eq!(b.timeslice_number, None);
676                assert!(b.channel_bonds.is_empty());
677                assert_eq!(b.reserved_tail, &[0xAA, 0xBB, 0xCC]);
678            }
679            other => panic!("expected S2X, got {other:?}"),
680        }
681        round_trip(&d);
682    }
683
684    #[test]
685    fn parse_s2x_mode3_channel_bonds() {
686        // mode 3 — 2 channel bonds (one with MIS/ISI, one without) + empty tail.
687        let b0 = 0x01 << 3;
688        let b1 = 0x03 << 6; // mode 3 [7:6], no scrambling, ts_gs 0
689        let mut sel = vec![b0, b1];
690        // Primary channel
691        sel.extend_from_slice(&0x1111_1111u32.to_be_bytes()); // frequency
692        sel.extend_from_slice(&0x0001u16.to_be_bytes()); // orbital
693        sel.push(0x00); // mis=0
694        sel.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // symbol_rate
695
696        // Bond count: reserved(7)=0 | num_channel_bonds_minus_one(1)=1 → 2 bonds
697        sel.push(0x01);
698
699        // Bond 0 (with MIS/ISI): frequency=0x22222222, orbital=0x0002,
700        //   we=1 pol=2(CircularLeft) mis=1 roll=3, symbol_rate=0x0ABCDEF, isi=0x77
701        sel.extend_from_slice(&0x2222_2222u32.to_be_bytes());
702        sel.extend_from_slice(&0x0002u16.to_be_bytes());
703        sel.push((1 << 7) | (0x02 << 5) | (1 << 4) | 0x03); // we=1 pol=2 mis=1 roll=3
704        let sr: u32 = 0x0AB_CDEF;
705        sel.push((sr >> 24) as u8 & 0x0F);
706        sel.push((sr >> 16) as u8);
707        sel.push((sr >> 8) as u8);
708        sel.push(sr as u8);
709        sel.push(0x77); // input_stream_identifier
710
711        // Bond 1 (no MIS): frequency=0x33333333, orbital=0x0003,
712        //   we=0 pol=1(LinearVertical) mis=0 roll=4, symbol_rate=0x0054321
713        sel.extend_from_slice(&0x3333_3333u32.to_be_bytes());
714        sel.extend_from_slice(&0x0003u16.to_be_bytes());
715        sel.push((0x01 << 5) | 0x04); // we=0 pol=1 mis=0 roll=4
716        let sr2: u32 = 0x005_4321;
717        sel.push((sr2 >> 24) as u8 & 0x0F);
718        sel.push((sr2 >> 16) as u8);
719        sel.push((sr2 >> 8) as u8);
720        sel.push(sr2 as u8);
721
722        let bytes = wrap(0x17, &sel);
723        let d = ExtensionDescriptor::parse(&bytes).unwrap();
724        match &d.body {
725            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
726                assert_eq!(b.s2x_mode, S2XMode::S2XChannelBonding);
727                assert_eq!(b.channel_bonds.len(), 2);
728
729                let b0 = &b.channel_bonds[0];
730                assert_eq!(b0.frequency, 0x2222_2222);
731                assert_eq!(b0.orbital_position, 0x0002);
732                assert!(b0.west_east_flag);
733                assert_eq!(b0.polarization, Polarization::CircularLeft);
734                assert!(b0.multiple_input_stream_flag);
735                assert_eq!(b0.roll_off, RollOff::Reserved(3));
736                assert_eq!(b0.symbol_rate, 0x0AB_CDEF);
737                assert_eq!(b0.input_stream_identifier, Some(0x77));
738
739                let b1 = &b.channel_bonds[1];
740                assert_eq!(b1.frequency, 0x3333_3333);
741                assert_eq!(b1.orbital_position, 0x0003);
742                assert!(!b1.west_east_flag);
743                assert_eq!(b1.polarization, Polarization::LinearVertical);
744                assert!(!b1.multiple_input_stream_flag);
745                assert_eq!(b1.roll_off, RollOff::Reserved(4));
746                assert_eq!(b1.symbol_rate, 0x005_4321);
747                assert_eq!(b1.input_stream_identifier, None);
748
749                assert!(b.reserved_tail.is_empty());
750            }
751            other => panic!("expected S2X, got {other:?}"),
752        }
753        round_trip(&d);
754    }
755
756    #[test]
757    fn tsduck_s2x_mode3_byte_exact() {
758        // TSDuck reference test-015: real s2x_mode==3 descriptor with
759        // scrambling, 2 channel bonds, and a 1-byte reserved tail.
760        let hex = "7f2a1750e3023456876543210037250456789601065432180340f600246754bd00654367123451000087642e";
761        let bytes = from_hex(hex);
762        let d = ExtensionDescriptor::parse(&bytes)
763            .unwrap_or_else(|e| panic!("parse tsduck s2x: {e:?}"));
764
765        assert_eq!(d.kind(), Some(ExtensionTag::S2XSatelliteDeliverySystem));
766        match &d.body {
767            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
768                assert_eq!(b.s2x_mode, S2XMode::S2XChannelBonding);
769                assert!(b.scrambling_sequence_selector);
770                assert_eq!(b.scrambling_sequence_index, Some(0x023456));
771                assert!(!b.channel_bonds.is_empty());
772                assert_eq!(b.channel_bonds.len(), 2);
773
774                let b0 = &b.channel_bonds[0];
775                assert_eq!(b0.frequency, 0x0654_3218);
776                assert_eq!(b0.orbital_position, 0x0340);
777                assert!(b0.west_east_flag);
778                assert_eq!(b0.polarization, Polarization::CircularRight);
779                assert!(b0.multiple_input_stream_flag);
780                assert_eq!(b0.roll_off, RollOff::Reserved(6));
781                assert_eq!(b0.symbol_rate, 0x0024_6754);
782                assert_eq!(b0.input_stream_identifier, Some(0xBD));
783
784                let b1 = &b.channel_bonds[1];
785                assert_eq!(b1.frequency, 0x0065_4367);
786                assert_eq!(b1.orbital_position, 0x1234);
787                assert!(!b1.west_east_flag);
788                assert_eq!(b1.polarization, Polarization::CircularLeft);
789                assert!(b1.multiple_input_stream_flag);
790                assert_eq!(b1.roll_off, RollOff::Alpha025);
791                assert_eq!(b1.symbol_rate, 0x0000_8764);
792                assert_eq!(b1.input_stream_identifier, Some(0x2E));
793
794                assert!(b.reserved_tail.is_empty());
795            }
796            other => panic!("expected S2X, got {other:?}"),
797        }
798
799        // Byte-exact round-trip: serialize must match input exactly
800        let mut out = vec![0u8; d.serialized_len()];
801        let n = d.serialize_into(&mut out).unwrap();
802        assert_eq!(
803            &out[..n],
804            &bytes[..],
805            "S2X mode 3 byte-exact re-serialize failed"
806        );
807    }
808}