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