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::*;
6
7impl<'a> ExtensionBodyDef<'a> for S2XSatelliteDeliverySystem<'a> {
8    const TAG_EXTENSION: u8 = 0x17;
9    const NAME: &'static str = "S2X_SATELLITE_DELIVERY_SYSTEM";
10}
11/// A single channel-bond entry (Table 140 inner `for` loop).
12///
13/// Layout mirrors the primary channel: frequency(4) + orbital_position(2) +
14/// packed byte + symbol_rate(4) + optional input_stream_identifier(1).
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize))]
17pub struct S2XChannelBond {
18    /// frequency(32).
19    pub frequency: u32,
20    /// orbital_position(16).
21    pub orbital_position: u16,
22    /// west_east_flag(1).
23    pub west_east_flag: bool,
24    /// polarization(2).
25    pub polarization: u8,
26    /// bonded_channel_multiple_input_stream_flag(1).
27    pub multiple_input_stream_flag: bool,
28    /// roll_off(3).
29    pub roll_off: u8,
30    /// symbol_rate(28).
31    pub symbol_rate: u32,
32    /// input_stream_identifier(8), present iff `multiple_input_stream_flag`.
33    pub input_stream_identifier: Option<u8>,
34}
35
36/// S2X_satellite_delivery_system body (Table 140).
37#[derive(Debug, Clone, PartialEq, Eq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize))]
39#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
40pub struct S2XSatelliteDeliverySystem<'a> {
41    /// receiver_profiles(5) — Table 141.
42    pub receiver_profiles: u8,
43    /// S2X_mode(2) — Table 142.
44    pub s2x_mode: u8,
45    /// scrambling_sequence_selector(1).
46    pub scrambling_sequence_selector: bool,
47    /// TS_GS_S2X_mode(2) — Table 143.
48    pub ts_gs_s2x_mode: u8,
49    /// scrambling_sequence_index(18), present iff `scrambling_sequence_selector`.
50    pub scrambling_sequence_index: Option<u32>,
51    /// frequency(32) — primary channel.
52    pub frequency: u32,
53    /// orbital_position(16).
54    pub orbital_position: u16,
55    /// west_east_flag(1).
56    pub west_east_flag: bool,
57    /// polarization(2).
58    pub polarization: u8,
59    /// multiple_input_stream_flag(1).
60    pub multiple_input_stream_flag: bool,
61    /// roll_off(3) — Table 144.
62    pub roll_off: u8,
63    /// symbol_rate(28).
64    pub symbol_rate: u32,
65    /// input_stream_identifier(8), present iff `multiple_input_stream_flag`.
66    pub input_stream_identifier: Option<u8>,
67    /// timeslice_number(8), present iff `s2x_mode == 2`.
68    pub timeslice_number: Option<u8>,
69    /// S2X_mode==3 channel-bond entries (empty unless s2x_mode==3).
70    pub channel_bonds: Vec<S2XChannelBond>,
71    /// Trailing reserved_future_use bytes (opaque), preserved verbatim.
72    pub reserved_tail: &'a [u8],
73}
74
75const BOND_BASE_LEN: usize = S2X_PRIMARY_LEN;
76
77fn parse_channel_common(
78    sel: &[u8],
79    pos: &mut usize,
80) -> Result<(u32, u16, bool, u8, bool, u8, u32)> {
81    if sel.len() < *pos + BOND_BASE_LEN {
82        return Err(Error::BufferTooShort {
83            need: *pos + BOND_BASE_LEN,
84            have: sel.len(),
85            what: "S2X body",
86        });
87    }
88    let frequency = u32::from_be_bytes([sel[*pos], sel[*pos + 1], sel[*pos + 2], sel[*pos + 3]]);
89    let orbital_position = u16::from_be_bytes([sel[*pos + 4], sel[*pos + 5]]);
90    let pb = sel[*pos + 6];
91    let west_east_flag = (pb & 0x80) != 0;
92    let polarization = (pb >> 5) & 0x03;
93    let multiple_input_stream_flag = (pb & 0x10) != 0;
94    let roll_off = pb & 0x07;
95    let symbol_rate = (u32::from(sel[*pos + 7] & 0x0F) << 24)
96        | (u32::from(sel[*pos + 8]) << 16)
97        | (u32::from(sel[*pos + 9]) << 8)
98        | u32::from(sel[*pos + 10]);
99    *pos += BOND_BASE_LEN;
100    Ok((
101        frequency,
102        orbital_position,
103        west_east_flag,
104        polarization,
105        multiple_input_stream_flag,
106        roll_off,
107        symbol_rate,
108    ))
109}
110
111fn write_channel_common(
112    buf: &mut [u8],
113    p: &mut usize,
114    frequency: u32,
115    orbital_position: u16,
116    packed: u8,
117    symbol_rate: u32,
118) {
119    buf[*p..*p + 4].copy_from_slice(&frequency.to_be_bytes());
120    buf[*p + 4..*p + 6].copy_from_slice(&orbital_position.to_be_bytes());
121    buf[*p + 6] = packed;
122    let sr = symbol_rate & 0x0FFF_FFFF;
123    buf[*p + 7] = (sr >> 24) as u8 & 0x0F;
124    buf[*p + 8] = (sr >> 16) as u8;
125    buf[*p + 9] = (sr >> 8) as u8;
126    buf[*p + 10] = sr as u8;
127    *p += BOND_BASE_LEN;
128}
129
130fn pack_we_pol_mis_ro(we: bool, pol: u8, mis: bool, ro: u8) -> u8 {
131    (u8::from(we) << 7) | ((pol & 0x03) << 5) | (u8::from(mis) << 4) | (ro & 0x07)
132}
133
134impl<'a> Parse<'a> for S2XSatelliteDeliverySystem<'a> {
135    type Error = crate::error::Error;
136    fn parse(sel: &'a [u8]) -> Result<Self> {
137        // receiver_profiles byte + S2X mode/flags byte = 2 fixed bytes.
138        if sel.len() < 2 {
139            return Err(Error::BufferTooShort {
140                need: 2,
141                have: sel.len(),
142                what: "S2X body",
143            });
144        }
145        let receiver_profiles = sel[0] >> 3;
146        let b1 = sel[1];
147        // Table 140 byte 1, MSB-first: S2X_mode(2) scrambling_sequence_selector(1)
148        // reserved_zero_future_use(3) TS_GS_S2X_mode(2).
149        let s2x_mode = (b1 >> 6) & 0x03;
150        let scrambling_sequence_selector = (b1 & 0x20) != 0;
151        let ts_gs_s2x_mode = b1 & 0x03;
152        let mut pos = 2;
153        let scrambling_sequence_index = if scrambling_sequence_selector {
154            if sel.len() < pos + S2X_SCRAMBLING_LEN {
155                return Err(Error::BufferTooShort {
156                    need: pos + S2X_SCRAMBLING_LEN,
157                    have: sel.len(),
158                    what: "S2X body",
159                });
160            }
161            let idx = (u32::from(sel[pos] & 0x03) << 16)
162                | (u32::from(sel[pos + 1]) << 8)
163                | u32::from(sel[pos + 2]);
164            pos += S2X_SCRAMBLING_LEN;
165            Some(idx)
166        } else {
167            None
168        };
169        // Primary channel (Table 140): frequency(32) orbital_position(16)
170        //   packed byte = west_east(1) polarization(2) mis(1) reserved(1) roll_off(3)
171        //   then reserved(4) | symbol_rate[27:24], and 3 bytes symbol_rate[23:0].
172        let (
173            frequency,
174            orbital_position,
175            west_east_flag,
176            polarization,
177            multiple_input_stream_flag,
178            roll_off,
179            symbol_rate,
180        ) = parse_channel_common(sel, &mut pos)?;
181        let input_stream_identifier = if multiple_input_stream_flag {
182            if sel.len() < pos + 1 {
183                return Err(Error::BufferTooShort {
184                    need: pos + 1,
185                    have: sel.len(),
186                    what: "S2X body",
187                });
188            }
189            let isi = sel[pos];
190            pos += 1;
191            Some(isi)
192        } else {
193            None
194        };
195        let timeslice_number = if s2x_mode == 2 {
196            if sel.len() < pos + 1 {
197                return Err(Error::BufferTooShort {
198                    need: pos + 1,
199                    have: sel.len(),
200                    what: "S2X body",
201                });
202            }
203            let ts = sel[pos];
204            pos += 1;
205            Some(ts)
206        } else {
207            None
208        };
209        let (channel_bonds, reserved_tail) = if s2x_mode == 3 {
210            // --- channel bonding loop (Table 140) ---
211            if sel.len() < pos + 1 {
212                return Err(Error::BufferTooShort {
213                    need: pos + 1,
214                    have: sel.len(),
215                    what: "S2X body",
216                });
217            }
218            let bond_byte = sel[pos];
219            pos += 1;
220            // reserved_zero_future_use(7) | num_channel_bonds_minus_one(1)
221            let num_channel_bonds = (bond_byte & 0x01) as usize + 1;
222            let mut bonds = Vec::with_capacity(num_channel_bonds);
223            for _ in 0..num_channel_bonds {
224                let (freq, orb, we, pol, mis, ro, sr) = parse_channel_common(sel, &mut pos)?;
225                let isi = if mis {
226                    if sel.len() < pos + 1 {
227                        return Err(Error::BufferTooShort {
228                            need: pos + 1,
229                            have: sel.len(),
230                            what: "S2X body",
231                        });
232                    }
233                    let v = sel[pos];
234                    pos += 1;
235                    Some(v)
236                } else {
237                    None
238                };
239                bonds.push(S2XChannelBond {
240                    frequency: freq,
241                    orbital_position: orb,
242                    west_east_flag: we,
243                    polarization: pol,
244                    multiple_input_stream_flag: mis,
245                    roll_off: ro,
246                    symbol_rate: sr,
247                    input_stream_identifier: isi,
248                });
249            }
250            (bonds, &sel[pos..])
251        } else {
252            (Vec::new(), &sel[pos..])
253        };
254        Ok(S2XSatelliteDeliverySystem {
255            receiver_profiles,
256            s2x_mode,
257            scrambling_sequence_selector,
258            ts_gs_s2x_mode,
259            scrambling_sequence_index,
260            frequency,
261            orbital_position,
262            west_east_flag,
263            polarization,
264            multiple_input_stream_flag,
265            roll_off,
266            symbol_rate,
267            input_stream_identifier,
268            timeslice_number,
269            channel_bonds,
270            reserved_tail,
271        })
272    }
273}
274
275impl Serialize for S2XSatelliteDeliverySystem<'_> {
276    type Error = crate::error::Error;
277    fn serialized_len(&self) -> usize {
278        let bond_len: usize = if self.s2x_mode == 3 {
279            1 + self
280                .channel_bonds
281                .iter()
282                .map(|b| BOND_BASE_LEN + usize::from(b.input_stream_identifier.is_some()))
283                .sum::<usize>()
284        } else {
285            0
286        };
287        2 + if self.scrambling_sequence_selector {
288            S2X_SCRAMBLING_LEN
289        } else {
290            0
291        } + S2X_PRIMARY_LEN
292            + usize::from(self.input_stream_identifier.is_some())
293            + usize::from(self.timeslice_number.is_some())
294            + bond_len
295            + self.reserved_tail.len()
296    }
297    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
298        let len = self.serialized_len();
299        if buf.len() < len {
300            return Err(Error::OutputBufferTooSmall {
301                need: len,
302                have: buf.len(),
303            });
304        }
305        buf[0] = self.receiver_profiles << 3;
306        buf[1] = ((self.s2x_mode & 0x03) << 6)
307            | (u8::from(self.scrambling_sequence_selector) << 5)
308            | (self.ts_gs_s2x_mode & 0x03);
309        let mut p = 2;
310        if self.scrambling_sequence_selector {
311            let idx = self.scrambling_sequence_index.unwrap_or(0) & 0x3FFFF;
312            buf[p] = (idx >> 16) as u8 & 0x03;
313            buf[p + 1] = (idx >> 8) as u8;
314            buf[p + 2] = idx as u8;
315            p += S2X_SCRAMBLING_LEN;
316        }
317        write_channel_common(
318            buf,
319            &mut p,
320            self.frequency,
321            self.orbital_position,
322            pack_we_pol_mis_ro(
323                self.west_east_flag,
324                self.polarization,
325                self.multiple_input_stream_flag,
326                self.roll_off,
327            ),
328            self.symbol_rate,
329        );
330        if let Some(isi) = self.input_stream_identifier {
331            buf[p] = isi;
332            p += 1;
333        }
334        if let Some(ts) = self.timeslice_number {
335            buf[p] = ts;
336            p += 1;
337        }
338        if self.s2x_mode == 3 {
339            // reserved_zero_future_use(7) | num_channel_bonds_minus_one(1)
340            buf[p] = (self.channel_bonds.len() as u8).saturating_sub(1) & 0x01;
341            p += 1;
342            for bond in &self.channel_bonds {
343                write_channel_common(
344                    buf,
345                    &mut p,
346                    bond.frequency,
347                    bond.orbital_position,
348                    pack_we_pol_mis_ro(
349                        bond.west_east_flag,
350                        bond.polarization,
351                        bond.multiple_input_stream_flag,
352                        bond.roll_off,
353                    ),
354                    bond.symbol_rate,
355                );
356                if let Some(isi) = bond.input_stream_identifier {
357                    buf[p] = isi;
358                    p += 1;
359                }
360            }
361        }
362        buf[p..p + self.reserved_tail.len()].copy_from_slice(self.reserved_tail);
363        Ok(len)
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370    use crate::descriptors::extension::test_support::*;
371    use crate::descriptors::extension::{ExtensionBody, ExtensionDescriptor};
372
373    #[test]
374    fn parse_s2x_primary_with_isi_and_timeslice() {
375        // receiver_profiles=0x05; s2x_mode=2, scram_sel=0, ts_gs=1; ISI + timeslice
376        let b0 = 0x05 << 3;
377        let b1 = (0x02 << 6) | 0x01; // mode 2 [7:6], no scrambling, ts_gs 1 [1:0]
378        let mut sel = vec![b0, b1];
379        sel.extend_from_slice(&0x0102_0304u32.to_be_bytes()); // frequency
380        sel.extend_from_slice(&0x00C8u16.to_be_bytes()); // orbital_position
381        sel.push((1 << 7) | (0x02 << 5) | (1 << 4) | 0x03); // we=1 pol=2 mis=1 roll=3
382        let sr: u32 = 0x0AB_CDEF; // symbol_rate (28-bit)
383        sel.push((sr >> 24) as u8 & 0x0F);
384        sel.push((sr >> 16) as u8);
385        sel.push((sr >> 8) as u8);
386        sel.push(sr as u8);
387        sel.push(0x42); // input_stream_identifier (mis=1)
388        sel.push(0x09); // timeslice_number (mode==2)
389        let bytes = wrap(0x17, &sel);
390        let d = ExtensionDescriptor::parse(&bytes).unwrap();
391        match &d.body {
392            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
393                assert_eq!(b.receiver_profiles, 0x05);
394                assert_eq!(b.s2x_mode, 2);
395                assert!(!b.scrambling_sequence_selector);
396                assert_eq!(b.ts_gs_s2x_mode, 1);
397                assert_eq!(b.frequency, 0x0102_0304);
398                assert_eq!(b.orbital_position, 0x00C8);
399                assert!(b.west_east_flag);
400                assert_eq!(b.polarization, 2);
401                assert!(b.multiple_input_stream_flag);
402                assert_eq!(b.roll_off, 3);
403                assert_eq!(b.symbol_rate, 0x0AB_CDEF);
404                assert_eq!(b.input_stream_identifier, Some(0x42));
405                assert_eq!(b.timeslice_number, Some(0x09));
406                assert!(b.channel_bonds.is_empty());
407                assert!(b.reserved_tail.is_empty());
408            }
409            other => panic!("expected S2X, got {other:?}"),
410        }
411        round_trip(&d);
412    }
413
414    #[test]
415    fn parse_s2x_with_scrambling_index() {
416        let b0 = 0x01 << 3;
417        let b1 = (0x01 << 6) | 0x20; // mode 1 [7:6], scrambling selector set [5]
418        let mut sel = vec![b0, b1];
419        // scrambling index 0x2ABCD (18-bit)
420        sel.push(0x02);
421        sel.push(0xAB);
422        sel.push(0xCD);
423        sel.extend_from_slice(&0u32.to_be_bytes()); // frequency
424        sel.extend_from_slice(&0u16.to_be_bytes()); // orbital
425        sel.push(0x00); // packed (mis=0)
426        sel.extend_from_slice(&[0, 0, 0, 0]); // symbol_rate
427        let bytes = wrap(0x17, &sel);
428        let d = ExtensionDescriptor::parse(&bytes).unwrap();
429        match &d.body {
430            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
431                assert!(b.scrambling_sequence_selector);
432                assert_eq!(b.scrambling_sequence_index, Some(0x2ABCD));
433                assert_eq!(b.input_stream_identifier, None);
434                assert_eq!(b.timeslice_number, None);
435                assert!(b.channel_bonds.is_empty());
436                assert!(b.reserved_tail.is_empty());
437            }
438            other => panic!("expected S2X, got {other:?}"),
439        }
440        round_trip(&d);
441    }
442
443    #[test]
444    fn parse_s2x_mode1_tail_preserved() {
445        // mode 1 — no channel bonds; trailing bytes become reserved_tail.
446        let b0 = 0x01 << 3;
447        let b1 = 0x01 << 6; // mode 1 [7:6], no scrambling, ts_gs 0
448        let mut sel = vec![b0, b1];
449        sel.extend_from_slice(&0u32.to_be_bytes());
450        sel.extend_from_slice(&0u16.to_be_bytes());
451        sel.push(0x00); // mis=0
452        sel.extend_from_slice(&[0, 0, 0, 0]); // symbol_rate
453        sel.extend_from_slice(&[0xAA, 0xBB, 0xCC]); // reserved_future_use tail
454        let bytes = wrap(0x17, &sel);
455        let d = ExtensionDescriptor::parse(&bytes).unwrap();
456        match &d.body {
457            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
458                assert_eq!(b.s2x_mode, 1);
459                assert_eq!(b.timeslice_number, None);
460                assert!(b.channel_bonds.is_empty());
461                assert_eq!(b.reserved_tail, &[0xAA, 0xBB, 0xCC]);
462            }
463            other => panic!("expected S2X, got {other:?}"),
464        }
465        round_trip(&d);
466    }
467
468    #[test]
469    fn parse_s2x_mode3_channel_bonds() {
470        // mode 3 — 2 channel bonds (one with MIS/ISI, one without) + empty tail.
471        let b0 = 0x01 << 3;
472        let b1 = 0x03 << 6; // mode 3 [7:6], no scrambling, ts_gs 0
473        let mut sel = vec![b0, b1];
474        // Primary channel
475        sel.extend_from_slice(&0x1111_1111u32.to_be_bytes()); // frequency
476        sel.extend_from_slice(&0x0001u16.to_be_bytes()); // orbital
477        sel.push(0x00); // mis=0
478        sel.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // symbol_rate
479
480        // Bond count: reserved(7)=0 | num_channel_bonds_minus_one(1)=1 → 2 bonds
481        sel.push(0x01);
482
483        // Bond 0 (with MIS/ISI): frequency=0x22222222, orbital=0x0002,
484        //   we=1 pol=2 mis=1 roll=3, symbol_rate=0x0ABCDEF, isi=0x77
485        sel.extend_from_slice(&0x2222_2222u32.to_be_bytes());
486        sel.extend_from_slice(&0x0002u16.to_be_bytes());
487        sel.push((1 << 7) | (0x02 << 5) | (1 << 4) | 0x03); // we=1 pol=2 mis=1 roll=3
488        let sr: u32 = 0x0AB_CDEF;
489        sel.push((sr >> 24) as u8 & 0x0F);
490        sel.push((sr >> 16) as u8);
491        sel.push((sr >> 8) as u8);
492        sel.push(sr as u8);
493        sel.push(0x77); // input_stream_identifier
494
495        // Bond 1 (no MIS): frequency=0x33333333, orbital=0x0003,
496        //   we=0 pol=1 mis=0 roll=4, symbol_rate=0x0054321
497        sel.extend_from_slice(&0x3333_3333u32.to_be_bytes());
498        sel.extend_from_slice(&0x0003u16.to_be_bytes());
499        sel.push((0x01 << 5) | 0x04); // we=0 pol=1 mis=0 roll=4
500        let sr2: u32 = 0x005_4321;
501        sel.push((sr2 >> 24) as u8 & 0x0F);
502        sel.push((sr2 >> 16) as u8);
503        sel.push((sr2 >> 8) as u8);
504        sel.push(sr2 as u8);
505
506        let bytes = wrap(0x17, &sel);
507        let d = ExtensionDescriptor::parse(&bytes).unwrap();
508        match &d.body {
509            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
510                assert_eq!(b.s2x_mode, 3);
511                assert_eq!(b.channel_bonds.len(), 2);
512
513                let b0 = &b.channel_bonds[0];
514                assert_eq!(b0.frequency, 0x2222_2222);
515                assert_eq!(b0.orbital_position, 0x0002);
516                assert!(b0.west_east_flag);
517                assert_eq!(b0.polarization, 2);
518                assert!(b0.multiple_input_stream_flag);
519                assert_eq!(b0.roll_off, 3);
520                assert_eq!(b0.symbol_rate, 0x0AB_CDEF);
521                assert_eq!(b0.input_stream_identifier, Some(0x77));
522
523                let b1 = &b.channel_bonds[1];
524                assert_eq!(b1.frequency, 0x3333_3333);
525                assert_eq!(b1.orbital_position, 0x0003);
526                assert!(!b1.west_east_flag);
527                assert_eq!(b1.polarization, 1);
528                assert!(!b1.multiple_input_stream_flag);
529                assert_eq!(b1.roll_off, 4);
530                assert_eq!(b1.symbol_rate, 0x005_4321);
531                assert_eq!(b1.input_stream_identifier, None);
532
533                assert!(b.reserved_tail.is_empty());
534            }
535            other => panic!("expected S2X, got {other:?}"),
536        }
537        round_trip(&d);
538    }
539
540    #[test]
541    fn tsduck_s2x_mode3_byte_exact() {
542        // TSDuck reference test-015: real s2x_mode==3 descriptor with
543        // scrambling, 2 channel bonds, and a 1-byte reserved tail.
544        let hex = "7f2a1750e3023456876543210037250456789601065432180340f600246754bd00654367123451000087642e";
545        let bytes = from_hex(hex);
546        let d = ExtensionDescriptor::parse(&bytes)
547            .unwrap_or_else(|e| panic!("parse tsduck s2x: {e:?}"));
548
549        assert_eq!(d.kind(), Some(ExtensionTag::S2XSatelliteDeliverySystem));
550        match &d.body {
551            ExtensionBody::S2XSatelliteDeliverySystem(b) => {
552                assert_eq!(b.s2x_mode, 3);
553                assert!(b.scrambling_sequence_selector);
554                assert_eq!(b.scrambling_sequence_index, Some(0x023456));
555                assert!(!b.channel_bonds.is_empty());
556                assert_eq!(b.channel_bonds.len(), 2);
557
558                let b0 = &b.channel_bonds[0];
559                assert_eq!(b0.frequency, 0x0654_3218);
560                assert_eq!(b0.orbital_position, 0x0340);
561                assert!(b0.west_east_flag);
562                assert_eq!(b0.polarization, 3);
563                assert!(b0.multiple_input_stream_flag);
564                assert_eq!(b0.roll_off, 6);
565                assert_eq!(b0.symbol_rate, 0x0024_6754);
566                assert_eq!(b0.input_stream_identifier, Some(0xBD));
567
568                let b1 = &b.channel_bonds[1];
569                assert_eq!(b1.frequency, 0x0065_4367);
570                assert_eq!(b1.orbital_position, 0x1234);
571                assert!(!b1.west_east_flag);
572                assert_eq!(b1.polarization, 2);
573                assert!(b1.multiple_input_stream_flag);
574                assert_eq!(b1.roll_off, 1);
575                assert_eq!(b1.symbol_rate, 0x0000_8764);
576                assert_eq!(b1.input_stream_identifier, Some(0x2E));
577
578                assert!(b.reserved_tail.is_empty());
579            }
580            other => panic!("expected S2X, got {other:?}"),
581        }
582
583        // Byte-exact round-trip: serialize must match input exactly
584        let mut out = vec![0u8; d.serialized_len()];
585        let n = d.serialize_into(&mut out).unwrap();
586        assert_eq!(
587            &out[..n],
588            &bytes[..],
589            "S2X mode 3 byte-exact re-serialize failed"
590        );
591    }
592}