Skip to main content

dvb_si/tables/
sat.rs

1//! Satellite Access Table (SAT) — ETSI EN 300 468 §5.2.11.
2//!
3//! Long-form private section on PID 0x001B with table_id 0x4D. The SAT is a
4//! *family*: a common `satellite_access_section()` header carries a 6-bit
5//! `satellite_table_id` discriminant ([`SatTableId`]) that selects one of five
6//! body structures (position v2, cell fragment, time association, beamhopping
7//! time plan, position v3).
8//!
9//! The body is typed as [`SatBody`] — an enum with one variant per defined
10//! layout plus a [`SatBody::Raw`] fallthrough for reserved
11//! `satellite_table_id` values 5–63. All five layouts use bit-packed fields; a
12//! private bit-level reader/writer handles the extraction and emission.
13
14use crate::error::{Error, Result};
15use alloc::vec;
16use alloc::vec::Vec;
17use dvb_common::{Parse, Serialize};
18
19/// table_id for the Satellite Access Table.
20pub const TABLE_ID: u8 = 0x4D;
21/// Well-known PID on which the SAT is carried (EN 300 468 Table 1, §5.1.3).
22pub const PID: u16 = 0x001B;
23
24const HEADER_LEN: usize = 9;
25const SECTION_LENGTH_PREFIX: usize = 3;
26const CRC_LEN: usize = 4;
27
28fn pad_to_byte(bits: usize) -> usize {
29    (8 - (bits % 8)) % 8
30}
31
32// ── Bit-level reader/writer ──────────────────────────────────────────────────
33
34struct BitReader<'a> {
35    data: &'a [u8],
36    bit_pos: usize,
37}
38
39impl<'a> BitReader<'a> {
40    fn new(data: &'a [u8]) -> Self {
41        Self { data, bit_pos: 0 }
42    }
43    fn remaining_bits(&self) -> usize {
44        (self.data.len() * 8).saturating_sub(self.bit_pos)
45    }
46    fn bits_consumed(&self) -> usize {
47        self.bit_pos
48    }
49    fn read_u(&mut self, bits: u8) -> Result<u64> {
50        let bits = bits as usize;
51        if self.bit_pos + bits > self.data.len() * 8 {
52            return Err(Error::BufferTooShort {
53                need: self.bit_pos + bits,
54                have: self.data.len() * 8,
55                what: "SatSection bit reader overrun",
56            });
57        }
58        let mut val: u64 = 0;
59        for i in 0..bits {
60            let byte_idx = (self.bit_pos + i) / 8;
61            let bit_idx = 7 - ((self.bit_pos + i) % 8);
62            val = (val << 1) | ((self.data[byte_idx] >> bit_idx) & 1) as u64;
63        }
64        self.bit_pos += bits;
65        Ok(val)
66    }
67    fn read_i(&mut self, bits: u8) -> Result<i64> {
68        let raw = self.read_u(bits)?;
69        let bits = bits as usize;
70        if raw & (1u64 << (bits - 1)) != 0 {
71            Ok((raw as i64) | (!0i64 << bits))
72        } else {
73            Ok(raw as i64)
74        }
75    }
76    fn skip(&mut self, bits: u8) -> Result<()> {
77        if self.bit_pos + bits as usize > self.data.len() * 8 {
78            return Err(Error::BufferTooShort {
79                need: self.bit_pos + bits as usize,
80                have: self.data.len() * 8,
81                what: "SatSection bit reader overrun",
82            });
83        }
84        self.bit_pos += bits as usize;
85        Ok(())
86    }
87}
88
89struct BitWriter<'a> {
90    buf: &'a mut [u8],
91    bit_pos: usize,
92}
93
94impl<'a> BitWriter<'a> {
95    fn new(buf: &'a mut [u8]) -> Self {
96        Self { buf, bit_pos: 0 }
97    }
98    fn bits_written(&self) -> usize {
99        self.bit_pos
100    }
101    fn write_u(&mut self, bits: u8, val: u64) -> Result<()> {
102        let bits = bits as usize;
103        if self.bit_pos + bits > self.buf.len() * 8 {
104            return Err(Error::BufferTooShort {
105                need: self.bit_pos + bits,
106                have: self.buf.len() * 8,
107                what: "SatSection bit writer overrun",
108            });
109        }
110        for i in 0..bits {
111            let byte_idx = (self.bit_pos + i) / 8;
112            let bit_idx = 7 - ((self.bit_pos + i) % 8);
113            let bit_val = ((val >> (bits - 1 - i)) & 1) as u8;
114            self.buf[byte_idx] |= bit_val << bit_idx;
115        }
116        self.bit_pos += bits;
117        Ok(())
118    }
119    fn write_i(&mut self, bits: u8, val: i64) -> Result<()> {
120        self.write_u(bits, val as u64 & ((1u64 << bits) - 1))
121    }
122    fn write_zero(&mut self, bits: u8) -> Result<()> {
123        if self.bit_pos + bits as usize > self.buf.len() * 8 {
124            return Err(Error::BufferTooShort {
125                need: self.bit_pos + bits as usize,
126                have: self.buf.len() * 8,
127                what: "SatSection bit writer overrun",
128            });
129        }
130        self.bit_pos += bits as usize;
131        Ok(())
132    }
133}
134
135// ── SatTableId discriminant ─────────────────────────────────────────────────
136
137/// `satellite_table_id` discriminant — selects the SAT body structure
138/// (§5.2.11.1, Table 11b).
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, num_enum::TryFromPrimitive)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize))]
141#[repr(u8)]
142#[non_exhaustive]
143pub enum SatTableId {
144    /// `satellite_position_v2_info` — TLE/SGP4 orbital elements (§5.2.11.2).
145    PositionV2 = 0,
146    /// `cell_fragment_info` — earth-surface cell coverage areas (§5.2.11.3).
147    CellFragment = 1,
148    /// `time_association_info` — NCR↔UTC time association (§5.2.11.4).
149    TimeAssociation = 2,
150    /// `beamhopping_time_plan_info` — beam illumination schedule (§5.2.11.5).
151    BeamhoppingTimePlan = 3,
152    /// `satellite_position_v3_info` — ephemeris state vectors (§5.2.11.6).
153    PositionV3 = 4,
154}
155
156// ── Position V2 (Table 11c) ─────────────────────────────────────────────────
157
158/// Position system selector for PositionV2.
159#[derive(Debug, Clone, PartialEq, Eq)]
160#[cfg_attr(feature = "serde", derive(serde::Serialize))]
161#[non_exhaustive]
162pub enum PositionSystem {
163    /// `position_system == 0`: orbital position (BCD 16-bit, west_east_flag).
164    Orbital {
165        /// `orbital_position` (16 bits, BCD-encoded as 4 digits).
166        orbital_position: u16,
167        /// `west_east_flag`.
168        west_east_flag: bool,
169    },
170    /// `position_system == 1`: SGP4 TLE elements.
171    Sgp4 {
172        /// `epoch_year` (8 bits).
173        epoch_year: u8,
174        /// `day_of_the_year` (16 bits).
175        day_of_the_year: u16,
176        /// `day_fraction` (32 bits, raw).
177        day_fraction: u32,
178        /// `mean_motion_first_derivative` (32 bits, raw spfmsbf).
179        mean_motion_first_derivative: u32,
180        /// `mean_motion_second_derivative` (32 bits, raw spfmsbf).
181        mean_motion_second_derivative: u32,
182        /// `drag_term` (32 bits, raw spfmsbf).
183        drag_term: u32,
184        /// `inclination` (32 bits, raw spfmsbf).
185        inclination: u32,
186        /// `right_ascension_of_the_ascending_node` (32 bits, raw spfmsbf).
187        right_ascension: u32,
188        /// `eccentricity` (32 bits, raw spfmsbf).
189        eccentricity: u32,
190        /// `argument_of_perigree` (32 bits, raw spfmsbf).
191        argument_of_perigree: u32,
192        /// `mean_anomaly` (32 bits, raw spfmsbf).
193        mean_anomaly: u32,
194        /// `mean_motion` (32 bits, raw spfmsbf).
195        mean_motion: u32,
196    },
197}
198
199/// A satellite entry in the PositionV2 body.
200#[derive(Debug, Clone, PartialEq, Eq)]
201#[cfg_attr(feature = "serde", derive(serde::Serialize))]
202pub struct PositionV2Satellite {
203    /// `satellite_id` (24 bits).
204    pub satellite_id: u32,
205    /// Position data (orbital or SGP4).
206    pub position: PositionSystem,
207}
208
209/// Position V2 body (Table 11c, §5.2.11.2).
210#[derive(Debug, Clone, PartialEq, Eq)]
211#[cfg_attr(feature = "serde", derive(serde::Serialize))]
212pub struct PositionV2Body {
213    /// Satellite entries.
214    pub satellites: Vec<PositionV2Satellite>,
215}
216
217// ── Cell Fragment (Table 11d) ────────────────────────────────────────────────
218
219/// Centre coordinates for a cell fragment (present when `first_occurrence == 1`).
220#[derive(Debug, Clone, PartialEq, Eq)]
221#[cfg_attr(feature = "serde", derive(serde::Serialize))]
222pub struct CellCenter {
223    /// `center_latitude` (18 bits, two's complement, `tcimsbf`).
224    pub center_latitude: i32,
225    /// `center_longitude` (19 bits, two's complement, `tcimsbf`).
226    pub center_longitude: i32,
227    /// `max_distance` (24 bits).
228    pub max_distance: u32,
229}
230
231/// A new delivery system entry in a cell fragment.
232#[derive(Debug, Clone, PartialEq, Eq)]
233#[cfg_attr(feature = "serde", derive(serde::Serialize))]
234pub struct NewDeliverySystem {
235    /// `new_delivery_system_id` (32 bits).
236    pub new_delivery_system_id: u32,
237    /// `time_of_application_base` (33 bits).
238    pub time_of_application_base: u64,
239    /// `time_of_application_ext` (9 bits).
240    pub time_of_application_ext: u16,
241}
242
243/// An obsolescent delivery system entry in a cell fragment.
244#[derive(Debug, Clone, PartialEq, Eq)]
245#[cfg_attr(feature = "serde", derive(serde::Serialize))]
246pub struct ObsolescentDeliverySystem {
247    /// `obsolescent_delivery_system_id` (32 bits).
248    pub obsolescent_delivery_system_id: u32,
249    /// `time_of_obsolescence_base` (33 bits).
250    pub time_of_obsolescence_base: u64,
251    /// `time_of_obsolescence_ext` (9 bits).
252    pub time_of_obsolescence_ext: u16,
253}
254
255/// A cell fragment entry (Table 11d, §5.2.11.3).
256#[derive(Debug, Clone, PartialEq, Eq)]
257#[cfg_attr(feature = "serde", derive(serde::Serialize))]
258pub struct CellFragment {
259    /// `cell_fragment_id` (32 bits).
260    pub cell_fragment_id: u32,
261    /// `first_occurrence`.
262    pub first_occurrence: bool,
263    /// `last_occurrence`.
264    pub last_occurrence: bool,
265    /// Centre coordinates (present iff `first_occurrence`).
266    pub center: Option<CellCenter>,
267    /// `delivery_system_id` entries (each 32 bits).
268    pub delivery_system_ids: Vec<u32>,
269    /// New delivery system entries.
270    pub new_delivery_systems: Vec<NewDeliverySystem>,
271    /// Obsolescent delivery system entries.
272    pub obsolescent_delivery_systems: Vec<ObsolescentDeliverySystem>,
273}
274
275/// Cell Fragment body (Table 11d, §5.2.11.3).
276#[derive(Debug, Clone, PartialEq, Eq)]
277#[cfg_attr(feature = "serde", derive(serde::Serialize))]
278pub struct CellFragmentBody {
279    /// Cell fragment entries.
280    pub fragments: Vec<CellFragment>,
281}
282
283// ── Time Association (Table 11e) ────────────────────────────────────────────
284
285/// Association type coding — ETSI EN 300 468 §5.2.11.4 Table 11f.
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287#[cfg_attr(feature = "serde", derive(serde::Serialize))]
288#[non_exhaustive]
289pub enum AssociationType {
290    /// 0 — UTC without leap second signalling.
291    UtcWithoutLeap,
292    /// 1 — UTC with leap second signalling.
293    UtcWithLeap,
294    /// 2..=15 — reserved.
295    Reserved(u8),
296}
297
298impl AssociationType {
299    #[must_use]
300    /// Decode from the wire value.  Every value maps (lossless).
301    pub fn from_u8(v: u8) -> Self {
302        match v & 0x0F {
303            0 => Self::UtcWithoutLeap,
304            1 => Self::UtcWithLeap,
305            v => Self::Reserved(v),
306        }
307    }
308
309    #[must_use]
310    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
311    pub fn to_u8(self) -> u8 {
312        match self {
313            Self::UtcWithoutLeap => 0,
314            Self::UtcWithLeap => 1,
315            Self::Reserved(v) => v,
316        }
317    }
318
319    #[must_use]
320    /// Human-readable spec display name.
321    pub fn name(self) -> &'static str {
322        match self {
323            Self::UtcWithoutLeap => "UTC without leap second",
324            Self::UtcWithLeap => "UTC with leap second",
325            Self::Reserved(_) => "Reserved",
326        }
327    }
328}
329dvb_common::impl_spec_display!(AssociationType, Reserved);
330
331/// Leap-second signalling info (present when `association_type == 1`).
332#[derive(Debug, Clone, PartialEq, Eq)]
333#[cfg_attr(feature = "serde", derive(serde::Serialize))]
334pub struct LeapInfo {
335    /// `leap59`.
336    pub leap59: bool,
337    /// `leap61`.
338    pub leap61: bool,
339    /// `pastleap59`.
340    pub pastleap59: bool,
341    /// `pastleap61`.
342    pub pastleap61: bool,
343}
344
345/// Time Association body (Table 11e, §5.2.11.4).
346#[derive(Debug, Clone, PartialEq, Eq)]
347#[cfg_attr(feature = "serde", derive(serde::Serialize))]
348pub struct TimeAssociationBody {
349    /// `association_type` (4 bits, Table 11f).
350    pub association_type: AssociationType,
351    /// Leap info (present iff `association_type == 1`).
352    pub leap_info: Option<LeapInfo>,
353    /// `ncr_base` (33 bits).
354    pub ncr_base: u64,
355    /// `ncr_ext` (9 bits).
356    pub ncr_ext: u16,
357    /// `association_timestamp_seconds` (64 bits).
358    pub association_timestamp_seconds: u64,
359    /// `association_timestamp_nanoseconds` (32 bits).
360    pub association_timestamp_nanoseconds: u32,
361}
362
363// ── Beamhopping Time Plan (Table 11g) ───────────────────────────────────────
364
365/// Time plan mode — ETSI EN 300 468 §5.2.11.5 Table 11g
366/// (`docs/en_300_468.md`, beamhopping_time_plan_info syntax, lines 706 ff.).
367///
368/// 2-bit field. Selects the body structure of a beamhopping plan entry.
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370#[cfg_attr(feature = "serde", derive(serde::Serialize))]
371#[non_exhaustive]
372pub enum TimePlanMode {
373    /// `0b00` — simple dwell/on-time plan (mode 0).
374    DwellOnTime,
375    /// `0b01` — bitmap plan (mode 1).
376    Bitmap,
377    /// `0b10` — grid/revisit/sleep plan (mode 2).
378    GridRevisitSleep,
379    /// `0b11` — reserved.
380    Reserved(u8),
381}
382
383impl TimePlanMode {
384    #[must_use]
385    /// Creates a value from a 2-bit wire nibble (upper bits masked off).
386    pub fn from_u8(v: u8) -> Self {
387        match v & 0x03 {
388            0 => Self::DwellOnTime,
389            1 => Self::Bitmap,
390            2 => Self::GridRevisitSleep,
391            v => Self::Reserved(v),
392        }
393    }
394
395    #[must_use]
396    /// Returns the 2-bit wire nibble for this value.
397    pub fn to_u8(self) -> u8 {
398        match self {
399            Self::DwellOnTime => 0,
400            Self::Bitmap => 1,
401            Self::GridRevisitSleep => 2,
402            Self::Reserved(v) => v,
403        }
404    }
405
406    #[must_use]
407    /// Returns the spec token for this value.
408    pub fn name(self) -> &'static str {
409        match self {
410            Self::DwellOnTime => "dwell/on-time",
411            Self::Bitmap => "bitmap",
412            Self::GridRevisitSleep => "grid/revisit/sleep",
413            Self::Reserved(_) => "reserved",
414        }
415    }
416}
417dvb_common::impl_spec_display!(TimePlanMode, Reserved);
418
419/// Mode-specific data in a beamhopping plan entry.
420#[derive(Debug, Clone, PartialEq, Eq)]
421#[cfg_attr(feature = "serde", derive(serde::Serialize))]
422#[non_exhaustive]
423pub enum BeamhoppingMode {
424    /// `time_plan_mode == 0`: simple dwell/on-time.
425    Mode0 {
426        /// `dwell_duration_base` (33 bits).
427        dwell_duration_base: u64,
428        /// `dwell_duration_ext` (9 bits).
429        dwell_duration_ext: u16,
430        /// `on_time_base` (33 bits).
431        on_time_base: u64,
432        /// `on_time_ext` (9 bits).
433        on_time_ext: u16,
434    },
435    /// `time_plan_mode == 1`: bitmap.
436    Mode1 {
437        /// `bit_map_size` (15 bits).
438        bit_map_size: u16,
439        /// `current_slot` (15 bits).
440        current_slot: u16,
441        /// `slot_transmission_on` flags (bit_map_size entries).
442        slot_transmission_on: Vec<bool>,
443    },
444    /// `time_plan_mode == 2`: grid/revisit/sleep.
445    Mode2 {
446        /// `grid_size_base` (33 bits).
447        grid_size_base: u64,
448        /// `grid_size_ext` (9 bits).
449        grid_size_ext: u16,
450        /// `revisit_duration_base` (33 bits).
451        revisit_duration_base: u64,
452        /// `revisit_duration_ext` (9 bits).
453        revisit_duration_ext: u16,
454        /// `sleep_time_base` (33 bits).
455        sleep_time_base: u64,
456        /// `sleep_time_ext` (9 bits).
457        sleep_time_ext: u16,
458        /// `sleep_duration_base` (33 bits).
459        sleep_duration_base: u64,
460        /// `sleep_duration_ext` (9 bits).
461        sleep_duration_ext: u16,
462    },
463    /// Reserved `time_plan_mode` (3): raw body bytes between the common
464    /// header and the plan boundary, preserved for byte-exact round-trip.
465    Reserved(Vec<u8>),
466}
467
468/// A beamhopping plan entry.
469#[derive(Debug, Clone, PartialEq, Eq)]
470#[cfg_attr(feature = "serde", derive(serde::Serialize))]
471pub struct BeamhoppingPlan {
472    /// `beamhopping_time_plan_id` (32 bits).
473    pub beamhopping_time_plan_id: u32,
474    /// `time_plan_mode` (2 bits) — [`TimePlanMode`].
475    pub time_plan_mode: TimePlanMode,
476    /// `time_of_application_base` (33 bits).
477    pub time_of_application_base: u64,
478    /// `time_of_application_ext` (9 bits).
479    pub time_of_application_ext: u16,
480    /// `cycle_duration_base` (33 bits).
481    pub cycle_duration_base: u64,
482    /// `cycle_duration_ext` (9 bits).
483    pub cycle_duration_ext: u16,
484    /// Mode-specific data.
485    pub mode: BeamhoppingMode,
486}
487
488/// Beamhopping Time Plan body (Table 11g, §5.2.11.5).
489#[derive(Debug, Clone, PartialEq, Eq)]
490#[cfg_attr(feature = "serde", derive(serde::Serialize))]
491pub struct BeamhoppingTimePlanBody {
492    /// Plan entries.
493    pub plans: Vec<BeamhoppingPlan>,
494}
495
496// ── Position V3 (Table 11h) ─────────────────────────────────────────────────
497
498/// Usable time range (optional, within metadata).
499#[derive(Debug, Clone, PartialEq, Eq)]
500#[cfg_attr(feature = "serde", derive(serde::Serialize))]
501pub struct UsableTime {
502    /// `year` (8 bits).
503    pub year: u8,
504    /// `day` (9 bits).
505    pub day: u16,
506    /// `day_fraction` (32 bits, spfmsbf raw).
507    pub day_fraction: u32,
508}
509
510/// Metadata block (optional, within a V3 satellite entry).
511#[derive(Debug, Clone, PartialEq, Eq)]
512#[cfg_attr(feature = "serde", derive(serde::Serialize))]
513pub struct PositionV3Metadata {
514    /// `total_start_time_year` (8 bits).
515    pub total_start_time_year: u8,
516    /// `total_start_time_day` (9 bits).
517    pub total_start_time_day: u16,
518    /// `total_start_time_day_fraction` (32 bits).
519    pub total_start_time_day_fraction: u32,
520    /// `total_stop_time_year` (8 bits).
521    pub total_stop_time_year: u8,
522    /// `total_stop_time_day` (9 bits).
523    pub total_stop_time_day: u16,
524    /// `total_stop_time_day_fraction` (32 bits).
525    pub total_stop_time_day_fraction: u32,
526    /// `interpolation_flag` — 1 bit.
527    pub interpolation_flag: bool,
528    /// `interpolation_type` (3 bits, Table 11i).
529    pub interpolation_type: InterpolationType,
530    /// `interpolation_degree` (3 bits).
531    pub interpolation_degree: u8,
532    /// Usable start time (optional).
533    pub usable_start_time: Option<UsableTime>,
534    /// Usable stop time (optional).
535    pub usable_stop_time: Option<UsableTime>,
536}
537
538/// Interpolation type coding — ETSI EN 300 468 §5.2.11.6 Table 11i.
539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540#[cfg_attr(feature = "serde", derive(serde::Serialize))]
541#[non_exhaustive]
542pub enum InterpolationType {
543    /// 0 — Reserved.
544    Reserved0,
545    /// 1 — Linear.
546    Linear,
547    /// 2 — Lagrange.
548    Lagrange,
549    /// 3 — Reserved.
550    Reserved3,
551    /// 4 — Hermite.
552    Hermite,
553    /// 5..=7 — Reserved.
554    ReservedOther(u8),
555}
556
557impl InterpolationType {
558    #[must_use]
559    /// Decode from the wire value.  Every value maps (lossless).
560    pub fn from_u8(v: u8) -> Self {
561        match v & 0x07 {
562            0 => Self::Reserved0,
563            1 => Self::Linear,
564            2 => Self::Lagrange,
565            3 => Self::Reserved3,
566            4 => Self::Hermite,
567            v => Self::ReservedOther(v),
568        }
569    }
570
571    #[must_use]
572    /// Encode to the wire value.  Inverse of `from_u8` / `from_u16`.
573    pub fn to_u8(self) -> u8 {
574        match self {
575            Self::Reserved0 => 0,
576            Self::Linear => 1,
577            Self::Lagrange => 2,
578            Self::Reserved3 => 3,
579            Self::Hermite => 4,
580            Self::ReservedOther(v) => v,
581        }
582    }
583
584    #[must_use]
585    /// Human-readable spec display name.
586    pub fn name(self) -> &'static str {
587        match self {
588            Self::Reserved0 => "Reserved",
589            Self::Linear => "Linear",
590            Self::Lagrange => "Lagrange",
591            Self::Reserved3 => "Reserved",
592            Self::Hermite => "Hermite",
593            Self::ReservedOther(_) => "Reserved",
594        }
595    }
596}
597dvb_common::impl_spec_display!(InterpolationType, ReservedOther);
598
599/// Ephemeris acceleration (optional, 3 × 32-bit spfmsbf).
600#[derive(Debug, Clone, PartialEq, Eq)]
601#[cfg_attr(feature = "serde", derive(serde::Serialize))]
602pub struct EphemerisAccel {
603    /// `ephemeris_x_ddot` (32 bits, spfmsbf raw).
604    pub ephemeris_x_ddot: u32,
605    /// `ephemeris_y_ddot` (32 bits, spfmsbf raw).
606    pub ephemeris_y_ddot: u32,
607    /// `ephemeris_z_ddot` (32 bits, spfmsbf raw).
608    pub ephemeris_z_ddot: u32,
609}
610
611/// A single ephemeris data point.
612#[derive(Debug, Clone, PartialEq, Eq)]
613#[cfg_attr(feature = "serde", derive(serde::Serialize))]
614pub struct EphemerisData {
615    /// `epoch_year` (8 bits).
616    pub epoch_year: u8,
617    /// `epoch_day` (9 bits).
618    pub epoch_day: u16,
619    /// `epoch_day_fraction` (32 bits).
620    pub epoch_day_fraction: u32,
621    /// `ephemeris_x` (32 bits, spfmsbf raw).
622    pub ephemeris_x: u32,
623    /// `ephemeris_y` (32 bits, spfmsbf raw).
624    pub ephemeris_y: u32,
625    /// `ephemeris_z` (32 bits, spfmsbf raw).
626    pub ephemeris_z: u32,
627    /// `ephemeris_x_dot` (32 bits, spfmsbf raw).
628    pub ephemeris_x_dot: u32,
629    /// `ephemeris_y_dot` (32 bits, spfmsbf raw).
630    pub ephemeris_y_dot: u32,
631    /// `ephemeris_z_dot` (32 bits, spfmsbf raw).
632    pub ephemeris_z_dot: u32,
633    /// Acceleration (optional).
634    pub acceleration: Option<EphemerisAccel>,
635}
636
637/// Covariance data (21 × 32-bit elements).
638#[derive(Debug, Clone, PartialEq, Eq)]
639#[cfg_attr(feature = "serde", derive(serde::Serialize))]
640pub struct CovarianceData {
641    /// `covariance_epoch_year` (8 bits).
642    pub covariance_epoch_year: u8,
643    /// `covariance_epoch_day` (9 bits).
644    pub covariance_epoch_day: u16,
645    /// `covariance_epoch_day_fraction` (32 bits).
646    pub covariance_epoch_day_fraction: u32,
647    /// 21 covariance elements (each 32 bits, spfmsbf raw).
648    pub covariance_elements: [u32; 21],
649}
650
651/// A satellite entry in the PositionV3 body.
652#[derive(Debug, Clone, PartialEq, Eq)]
653#[cfg_attr(feature = "serde", derive(serde::Serialize))]
654pub struct PositionV3Satellite {
655    /// `satellite_id` (24 bits).
656    pub satellite_id: u32,
657    /// `usable_start_time_flag`.
658    pub usable_start_time_flag: bool,
659    /// `usable_stop_time_flag`.
660    pub usable_stop_time_flag: bool,
661    /// `ephemeris_accel_flag`.
662    pub ephemeris_accel_flag: bool,
663    /// `covariance_flag`.
664    pub covariance_flag: bool,
665    /// Metadata block (optional); its presence also drives the
666    /// `metadata_flag` bit on the wire.
667    pub metadata: Option<PositionV3Metadata>,
668    /// Ephemeris data entries; their count is derived on serialization.
669    pub ephemeris_data: Vec<EphemerisData>,
670    /// Covariance data (optional).
671    pub covariance: Option<CovarianceData>,
672}
673
674/// Position V3 body (Table 11h, §5.2.11.6).
675#[derive(Debug, Clone, PartialEq, Eq)]
676#[cfg_attr(feature = "serde", derive(serde::Serialize))]
677pub struct PositionV3Body {
678    /// `oem_version_major` (4 bits).
679    pub oem_version_major: u8,
680    /// `oem_version_minor` (4 bits).
681    pub oem_version_minor: u8,
682    /// `creation_date_year` (8 bits).
683    pub creation_date_year: u8,
684    /// `creation_date_day` (9 bits).
685    pub creation_date_day: u16,
686    /// `creation_date_day_fraction` (32 bits).
687    pub creation_date_day_fraction: u32,
688    /// Satellite entries.
689    pub satellites: Vec<PositionV3Satellite>,
690}
691
692// ── SatBody enum ────────────────────────────────────────────────────────────
693
694/// The typed body of a SAT section, selected by `satellite_table_id`
695/// (Tables 11c–11h).
696#[derive(Debug, Clone, PartialEq, Eq)]
697#[cfg_attr(feature = "serde", derive(serde::Serialize))]
698#[non_exhaustive]
699pub enum SatBody {
700    /// `satellite_table_id == 0`: Position V2 (Table 11c).
701    PositionV2(PositionV2Body),
702    /// `satellite_table_id == 1`: Cell Fragment (Table 11d).
703    CellFragment(CellFragmentBody),
704    /// `satellite_table_id == 2`: Time Association (Table 11e).
705    TimeAssociation(TimeAssociationBody),
706    /// `satellite_table_id == 3`: Beamhopping Time Plan (Table 11g).
707    BeamhoppingTimePlan(BeamhoppingTimePlanBody),
708    /// `satellite_table_id == 4`: Position V3 (Table 11h).
709    PositionV3(PositionV3Body),
710    /// Reserved `satellite_table_id` (5–63): raw body bytes.
711    Raw(Vec<u8>),
712}
713
714fn sat_body_serialized_len(body: &SatBody) -> usize {
715    match body {
716        SatBody::Raw(v) => v.len(),
717        _ => {
718            // The hardened BitWriter errors (rather than silently truncating)
719            // on overrun, so feed it a buffer that's grown until the body fits.
720            // Real SAT bodies are bounded by the 12-bit section_length (<4 KiB);
721            // an over-large constructed body grows to the cap and is then
722            // rejected by the section_length guard in serialize_into — never a
723            // panic in this infallible length calc.
724            let mut cap = 4096usize;
725            loop {
726                let mut tmp = vec![0u8; cap];
727                let mut writer = BitWriter::new(&mut tmp);
728                if sat_body_write(body, &mut writer).is_ok() {
729                    break writer.bits_written().div_ceil(8);
730                }
731                if cap >= 1 << 20 {
732                    break cap; // pathological; serialize_into rejects it
733                }
734                cap *= 2;
735            }
736        }
737    }
738}
739
740fn sat_body_write(body: &SatBody, w: &mut BitWriter) -> Result<()> {
741    match body {
742        SatBody::PositionV2(b) => {
743            for sat in &b.satellites {
744                w.write_u(24, sat.satellite_id as u64)?;
745                w.write_zero(7)?;
746                match &sat.position {
747                    PositionSystem::Orbital {
748                        orbital_position,
749                        west_east_flag,
750                    } => {
751                        w.write_u(1, 0)?;
752                        w.write_u(16, *orbital_position as u64)?;
753                        w.write_u(1, *west_east_flag as u64)?;
754                        w.write_zero(7)?;
755                    }
756                    PositionSystem::Sgp4 {
757                        epoch_year,
758                        day_of_the_year,
759                        day_fraction,
760                        mean_motion_first_derivative,
761                        mean_motion_second_derivative,
762                        drag_term,
763                        inclination,
764                        right_ascension,
765                        eccentricity,
766                        argument_of_perigree,
767                        mean_anomaly,
768                        mean_motion,
769                    } => {
770                        w.write_u(1, 1)?;
771                        w.write_u(8, *epoch_year as u64)?;
772                        w.write_u(16, *day_of_the_year as u64)?;
773                        w.write_u(32, *day_fraction as u64)?;
774                        w.write_u(32, *mean_motion_first_derivative as u64)?;
775                        w.write_u(32, *mean_motion_second_derivative as u64)?;
776                        w.write_u(32, *drag_term as u64)?;
777                        w.write_u(32, *inclination as u64)?;
778                        w.write_u(32, *right_ascension as u64)?;
779                        w.write_u(32, *eccentricity as u64)?;
780                        w.write_u(32, *argument_of_perigree as u64)?;
781                        w.write_u(32, *mean_anomaly as u64)?;
782                        w.write_u(32, *mean_motion as u64)?;
783                    }
784                }
785            }
786        }
787        SatBody::CellFragment(b) => {
788            for frag in &b.fragments {
789                w.write_u(32, frag.cell_fragment_id as u64)?;
790                w.write_u(1, frag.first_occurrence as u64)?;
791                w.write_u(1, frag.last_occurrence as u64)?;
792                if frag.first_occurrence {
793                    if let Some(ref c) = frag.center {
794                        w.write_zero(4)?;
795                        w.write_i(18, c.center_latitude as i64)?;
796                        w.write_zero(5)?;
797                        w.write_i(19, c.center_longitude as i64)?;
798                        w.write_u(24, c.max_distance as u64)?;
799                        w.write_zero(6)?;
800                    }
801                } else {
802                    w.write_zero(4)?;
803                }
804                w.write_u(10, frag.delivery_system_ids.len() as u64)?;
805                for id in &frag.delivery_system_ids {
806                    w.write_u(32, *id as u64)?;
807                }
808                w.write_zero(6)?;
809                w.write_u(10, frag.new_delivery_systems.len() as u64)?;
810                for nds in &frag.new_delivery_systems {
811                    w.write_u(32, nds.new_delivery_system_id as u64)?;
812                    w.write_u(33, nds.time_of_application_base)?;
813                    w.write_zero(6)?;
814                    w.write_u(9, nds.time_of_application_ext as u64)?;
815                }
816                w.write_zero(6)?;
817                w.write_u(10, frag.obsolescent_delivery_systems.len() as u64)?;
818                for ods in &frag.obsolescent_delivery_systems {
819                    w.write_u(32, ods.obsolescent_delivery_system_id as u64)?;
820                    w.write_u(33, ods.time_of_obsolescence_base)?;
821                    w.write_zero(6)?;
822                    w.write_u(9, ods.time_of_obsolescence_ext as u64)?;
823                }
824            }
825        }
826        SatBody::TimeAssociation(b) => {
827            w.write_u(4, b.association_type.to_u8() as u64)?;
828            if b.association_type.to_u8() == 1 {
829                if let Some(ref li) = b.leap_info {
830                    w.write_u(1, li.leap59 as u64)?;
831                    w.write_u(1, li.leap61 as u64)?;
832                    w.write_u(1, li.pastleap59 as u64)?;
833                    w.write_u(1, li.pastleap61 as u64)?;
834                } else {
835                    w.write_zero(4)?;
836                }
837            } else {
838                w.write_zero(4)?;
839            }
840            w.write_u(33, b.ncr_base)?;
841            w.write_zero(6)?;
842            w.write_u(9, b.ncr_ext as u64)?;
843            w.write_u(64, b.association_timestamp_seconds)?;
844            w.write_u(32, b.association_timestamp_nanoseconds as u64)?;
845        }
846        SatBody::BeamhoppingTimePlan(b) => {
847            for plan in &b.plans {
848                w.write_u(32, plan.beamhopping_time_plan_id as u64)?;
849                w.write_zero(4)?;
850                let mode_bits = match &plan.mode {
851                    BeamhoppingMode::Mode0 { .. } => 33 + 6 + 9 + 33 + 6 + 9,
852                    BeamhoppingMode::Mode1 { bit_map_size, .. } => {
853                        let bm = *bit_map_size as usize;
854                        let raw = 1 + 15 + 1 + 15 + bm;
855                        raw + pad_to_byte(raw)
856                    }
857                    BeamhoppingMode::Mode2 { .. } => {
858                        33 + 6 + 9 + 33 + 6 + 9 + 33 + 6 + 9 + 33 + 6 + 9
859                    }
860                    BeamhoppingMode::Reserved(v) => v.len() * 8,
861                };
862                let total_bits_after_length = 8 + 48 + 48 + mode_bits;
863                let plan_length_bytes = total_bits_after_length / 8;
864                w.write_u(12, plan_length_bytes as u64)?;
865                w.write_zero(6)?;
866                w.write_u(2, plan.time_plan_mode.to_u8() as u64)?;
867                w.write_u(33, plan.time_of_application_base)?;
868                w.write_zero(6)?;
869                w.write_u(9, plan.time_of_application_ext as u64)?;
870                w.write_u(33, plan.cycle_duration_base)?;
871                w.write_zero(6)?;
872                w.write_u(9, plan.cycle_duration_ext as u64)?;
873                match &plan.mode {
874                    BeamhoppingMode::Mode0 {
875                        dwell_duration_base,
876                        dwell_duration_ext,
877                        on_time_base,
878                        on_time_ext,
879                    } => {
880                        w.write_u(33, *dwell_duration_base)?;
881                        w.write_zero(6)?;
882                        w.write_u(9, *dwell_duration_ext as u64)?;
883                        w.write_u(33, *on_time_base)?;
884                        w.write_zero(6)?;
885                        w.write_u(9, *on_time_ext as u64)?;
886                    }
887                    BeamhoppingMode::Mode1 {
888                        bit_map_size,
889                        current_slot,
890                        slot_transmission_on,
891                    } => {
892                        w.write_zero(1)?;
893                        w.write_u(15, *bit_map_size as u64)?;
894                        w.write_zero(1)?;
895                        w.write_u(15, *current_slot as u64)?;
896                        for &on in slot_transmission_on {
897                            w.write_u(1, on as u64)?;
898                        }
899                        let total = 1 + 15 + 1 + 15 + *bit_map_size as usize;
900                        for _ in 0..pad_to_byte(total) {
901                            w.write_zero(1)?;
902                        }
903                    }
904                    BeamhoppingMode::Mode2 {
905                        grid_size_base,
906                        grid_size_ext,
907                        revisit_duration_base,
908                        revisit_duration_ext,
909                        sleep_time_base,
910                        sleep_time_ext,
911                        sleep_duration_base,
912                        sleep_duration_ext,
913                    } => {
914                        w.write_u(33, *grid_size_base)?;
915                        w.write_zero(6)?;
916                        w.write_u(9, *grid_size_ext as u64)?;
917                        w.write_u(33, *revisit_duration_base)?;
918                        w.write_zero(6)?;
919                        w.write_u(9, *revisit_duration_ext as u64)?;
920                        w.write_u(33, *sleep_time_base)?;
921                        w.write_zero(6)?;
922                        w.write_u(9, *sleep_time_ext as u64)?;
923                        w.write_u(33, *sleep_duration_base)?;
924                        w.write_zero(6)?;
925                        w.write_u(9, *sleep_duration_ext as u64)?;
926                    }
927                    BeamhoppingMode::Reserved(v) => {
928                        for &b in v {
929                            w.write_u(8, b as u64)?;
930                        }
931                    }
932                }
933            }
934        }
935        SatBody::PositionV3(b) => {
936            w.write_u(4, b.oem_version_major as u64)?;
937            w.write_u(4, b.oem_version_minor as u64)?;
938            w.write_u(8, b.creation_date_year as u64)?;
939            w.write_zero(7)?;
940            w.write_u(9, b.creation_date_day as u64)?;
941            w.write_u(32, b.creation_date_day_fraction as u64)?;
942            for sat in &b.satellites {
943                w.write_u(24, sat.satellite_id as u64)?;
944                w.write_zero(3)?;
945                w.write_u(1, u8::from(sat.metadata.is_some()) as u64)?;
946                w.write_u(1, sat.usable_start_time_flag as u64)?;
947                w.write_u(1, sat.usable_stop_time_flag as u64)?;
948                w.write_u(1, sat.ephemeris_accel_flag as u64)?;
949                w.write_u(1, sat.covariance_flag as u64)?;
950                if let Some(ref md) = sat.metadata {
951                    w.write_u(8, md.total_start_time_year as u64)?;
952                    w.write_zero(7)?;
953                    w.write_u(9, md.total_start_time_day as u64)?;
954                    w.write_u(32, md.total_start_time_day_fraction as u64)?;
955                    w.write_u(8, md.total_stop_time_year as u64)?;
956                    w.write_zero(7)?;
957                    w.write_u(9, md.total_stop_time_day as u64)?;
958                    w.write_u(32, md.total_stop_time_day_fraction as u64)?;
959                    w.write_zero(1)?;
960                    w.write_u(1, md.interpolation_flag as u64)?;
961                    w.write_u(3, md.interpolation_type.to_u8() as u64)?;
962                    w.write_u(3, md.interpolation_degree as u64)?;
963                    if sat.usable_start_time_flag {
964                        if let Some(ref ut) = md.usable_start_time {
965                            w.write_u(8, ut.year as u64)?;
966                            w.write_zero(7)?;
967                            w.write_u(9, ut.day as u64)?;
968                            w.write_u(32, ut.day_fraction as u64)?;
969                        } else {
970                            w.write_zero(8)?;
971                            w.write_zero(7)?;
972                            w.write_zero(9)?;
973                            w.write_zero(32)?;
974                        }
975                    }
976                    if sat.usable_stop_time_flag {
977                        if let Some(ref ut) = md.usable_stop_time {
978                            w.write_u(8, ut.year as u64)?;
979                            w.write_zero(7)?;
980                            w.write_u(9, ut.day as u64)?;
981                            w.write_u(32, ut.day_fraction as u64)?;
982                        } else {
983                            w.write_zero(8)?;
984                            w.write_zero(7)?;
985                            w.write_zero(9)?;
986                            w.write_zero(32)?;
987                        }
988                    }
989                }
990                w.write_u(16, sat.ephemeris_data.len() as u64)?;
991                for ed in &sat.ephemeris_data {
992                    w.write_u(8, ed.epoch_year as u64)?;
993                    w.write_zero(7)?;
994                    w.write_u(9, ed.epoch_day as u64)?;
995                    w.write_u(32, ed.epoch_day_fraction as u64)?;
996                    w.write_u(32, ed.ephemeris_x as u64)?;
997                    w.write_u(32, ed.ephemeris_y as u64)?;
998                    w.write_u(32, ed.ephemeris_z as u64)?;
999                    w.write_u(32, ed.ephemeris_x_dot as u64)?;
1000                    w.write_u(32, ed.ephemeris_y_dot as u64)?;
1001                    w.write_u(32, ed.ephemeris_z_dot as u64)?;
1002                    if sat.ephemeris_accel_flag {
1003                        if let Some(ref acc) = ed.acceleration {
1004                            w.write_u(32, acc.ephemeris_x_ddot as u64)?;
1005                            w.write_u(32, acc.ephemeris_y_ddot as u64)?;
1006                            w.write_u(32, acc.ephemeris_z_ddot as u64)?;
1007                        } else {
1008                            w.write_zero(32)?;
1009                            w.write_zero(32)?;
1010                            w.write_zero(32)?;
1011                        }
1012                    }
1013                }
1014                if sat.covariance_flag {
1015                    if let Some(ref cov) = sat.covariance {
1016                        w.write_u(8, cov.covariance_epoch_year as u64)?;
1017                        w.write_zero(7)?;
1018                        w.write_u(9, cov.covariance_epoch_day as u64)?;
1019                        w.write_u(32, cov.covariance_epoch_day_fraction as u64)?;
1020                        for elem in &cov.covariance_elements {
1021                            w.write_u(32, *elem as u64)?;
1022                        }
1023                    } else {
1024                        w.write_zero(8)?;
1025                        w.write_zero(7)?;
1026                        w.write_zero(9)?;
1027                        w.write_zero(32)?;
1028                        for _ in 0..21 {
1029                            w.write_zero(32)?;
1030                        }
1031                    }
1032                }
1033            }
1034        }
1035        SatBody::Raw(_) => {}
1036    }
1037    Ok(())
1038}
1039
1040fn sat_body_parse(sat_table_id: u8, data: &[u8]) -> Result<SatBody> {
1041    if data.is_empty() && sat_table_id <= 4 {
1042        return Ok(match sat_table_id {
1043            0 => SatBody::PositionV2(PositionV2Body {
1044                satellites: Vec::new(),
1045            }),
1046            1 => SatBody::CellFragment(CellFragmentBody {
1047                fragments: Vec::new(),
1048            }),
1049            3 => SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody { plans: Vec::new() }),
1050            _ => {
1051                return Err(Error::BufferTooShort {
1052                    need: 1,
1053                    have: 0,
1054                    what: "SatSection body (non-loop type requires data)",
1055                });
1056            }
1057        });
1058    }
1059    let mut r = BitReader::new(data);
1060    match sat_table_id {
1061        0 => {
1062            let mut satellites = Vec::new();
1063            while r.remaining_bits() > 24 + 7 {
1064                let satellite_id = r.read_u(24)? as u32;
1065                r.skip(7)?;
1066                let position_system = r.read_u(1)?;
1067                let position = if position_system == 0 {
1068                    const ORBITAL_BITS: usize = 16 + 1 + 7;
1069                    if r.remaining_bits() < ORBITAL_BITS {
1070                        return Err(Error::BufferTooShort {
1071                            need: ORBITAL_BITS,
1072                            have: r.remaining_bits(),
1073                            what: "SatSection PositionV2 Orbital fields",
1074                        });
1075                    }
1076                    let orbital_position = r.read_u(16)? as u16;
1077                    let west_east_flag = r.read_u(1)? != 0;
1078                    r.skip(7)?;
1079                    PositionSystem::Orbital {
1080                        orbital_position,
1081                        west_east_flag,
1082                    }
1083                } else {
1084                    const SGP4_BITS: usize = 8 + 16 + 32 * 10;
1085                    if r.remaining_bits() < SGP4_BITS {
1086                        return Err(Error::BufferTooShort {
1087                            need: SGP4_BITS,
1088                            have: r.remaining_bits(),
1089                            what: "SatSection PositionV2 SGP4 fields",
1090                        });
1091                    }
1092                    let epoch_year = r.read_u(8)? as u8;
1093                    let day_of_the_year = r.read_u(16)? as u16;
1094                    let day_fraction = r.read_u(32)? as u32;
1095                    let mean_motion_first_derivative = r.read_u(32)? as u32;
1096                    let mean_motion_second_derivative = r.read_u(32)? as u32;
1097                    let drag_term = r.read_u(32)? as u32;
1098                    let inclination = r.read_u(32)? as u32;
1099                    let right_ascension = r.read_u(32)? as u32;
1100                    let eccentricity = r.read_u(32)? as u32;
1101                    let argument_of_perigree = r.read_u(32)? as u32;
1102                    let mean_anomaly = r.read_u(32)? as u32;
1103                    let mean_motion = r.read_u(32)? as u32;
1104                    PositionSystem::Sgp4 {
1105                        epoch_year,
1106                        day_of_the_year,
1107                        day_fraction,
1108                        mean_motion_first_derivative,
1109                        mean_motion_second_derivative,
1110                        drag_term,
1111                        inclination,
1112                        right_ascension,
1113                        eccentricity,
1114                        argument_of_perigree,
1115                        mean_anomaly,
1116                        mean_motion,
1117                    }
1118                };
1119                satellites.push(PositionV2Satellite {
1120                    satellite_id,
1121                    position,
1122                });
1123            }
1124            Ok(SatBody::PositionV2(PositionV2Body { satellites }))
1125        }
1126        1 => {
1127            let mut fragments = Vec::new();
1128            while r.remaining_bits() >= 32 + 2 {
1129                let cell_fragment_id = r.read_u(32)? as u32;
1130                let first_occurrence = r.read_u(1)? != 0;
1131                let last_occurrence = r.read_u(1)? != 0;
1132                let center = if first_occurrence {
1133                    const CENTER_BITS: usize = 4 + 18 + 5 + 19 + 24 + 6;
1134                    if r.remaining_bits() < CENTER_BITS {
1135                        return Err(Error::BufferTooShort {
1136                            need: CENTER_BITS,
1137                            have: r.remaining_bits(),
1138                            what: "SatSection CellFragment center",
1139                        });
1140                    }
1141                    r.skip(4)?;
1142                    let center_latitude = r.read_i(18)? as i32;
1143                    r.skip(5)?;
1144                    let center_longitude = r.read_i(19)? as i32;
1145                    let max_distance = r.read_u(24)? as u32;
1146                    r.skip(6)?;
1147                    Some(CellCenter {
1148                        center_latitude,
1149                        center_longitude,
1150                        max_distance,
1151                    })
1152                } else {
1153                    r.skip(4)?;
1154                    None
1155                };
1156                let dsid_count = r.read_u(10)? as usize;
1157                if r.remaining_bits() < dsid_count * 32 {
1158                    return Err(Error::BufferTooShort {
1159                        need: dsid_count * 32,
1160                        have: r.remaining_bits(),
1161                        what: "SatSection CellFragment delivery_system_ids",
1162                    });
1163                }
1164                let mut delivery_system_ids =
1165                    Vec::with_capacity(dsid_count.min(r.remaining_bits() / 32));
1166                for _ in 0..dsid_count {
1167                    delivery_system_ids.push(r.read_u(32)? as u32);
1168                }
1169                r.skip(6)?;
1170                let nds_count = r.read_u(10)? as usize;
1171                const NDS_ENTRY_BITS: usize = 32 + 33 + 6 + 9;
1172                if r.remaining_bits() < nds_count * NDS_ENTRY_BITS {
1173                    return Err(Error::BufferTooShort {
1174                        need: nds_count * NDS_ENTRY_BITS,
1175                        have: r.remaining_bits(),
1176                        what: "SatSection CellFragment new_delivery_systems",
1177                    });
1178                }
1179                let mut new_delivery_systems =
1180                    Vec::with_capacity(nds_count.min(r.remaining_bits() / NDS_ENTRY_BITS));
1181                for _ in 0..nds_count {
1182                    let new_delivery_system_id = r.read_u(32)? as u32;
1183                    let time_of_application_base = r.read_u(33)?;
1184                    r.skip(6)?;
1185                    let time_of_application_ext = r.read_u(9)? as u16;
1186                    new_delivery_systems.push(NewDeliverySystem {
1187                        new_delivery_system_id,
1188                        time_of_application_base,
1189                        time_of_application_ext,
1190                    });
1191                }
1192                r.skip(6)?;
1193                let ods_count = r.read_u(10)? as usize;
1194                if r.remaining_bits() < ods_count * NDS_ENTRY_BITS {
1195                    return Err(Error::BufferTooShort {
1196                        need: ods_count * NDS_ENTRY_BITS,
1197                        have: r.remaining_bits(),
1198                        what: "SatSection CellFragment obsolescent_delivery_systems",
1199                    });
1200                }
1201                let mut obsolescent_delivery_systems =
1202                    Vec::with_capacity(ods_count.min(r.remaining_bits() / NDS_ENTRY_BITS));
1203                for _ in 0..ods_count {
1204                    let obsolescent_delivery_system_id = r.read_u(32)? as u32;
1205                    let time_of_obsolescence_base = r.read_u(33)?;
1206                    r.skip(6)?;
1207                    let time_of_obsolescence_ext = r.read_u(9)? as u16;
1208                    obsolescent_delivery_systems.push(ObsolescentDeliverySystem {
1209                        obsolescent_delivery_system_id,
1210                        time_of_obsolescence_base,
1211                        time_of_obsolescence_ext,
1212                    });
1213                }
1214                fragments.push(CellFragment {
1215                    cell_fragment_id,
1216                    first_occurrence,
1217                    last_occurrence,
1218                    center,
1219                    delivery_system_ids,
1220                    new_delivery_systems,
1221                    obsolescent_delivery_systems,
1222                });
1223            }
1224            Ok(SatBody::CellFragment(CellFragmentBody { fragments }))
1225        }
1226        2 => {
1227            const TIME_ASSOC_MIN_BITS: usize = 4 + 4 + 33 + 6 + 9 + 64 + 32;
1228            if r.remaining_bits() < TIME_ASSOC_MIN_BITS {
1229                return Err(Error::BufferTooShort {
1230                    need: TIME_ASSOC_MIN_BITS,
1231                    have: r.remaining_bits(),
1232                    what: "SatSection TimeAssociation body",
1233                });
1234            }
1235            let association_type = AssociationType::from_u8(r.read_u(4)? as u8);
1236            let leap_info = if association_type.to_u8() == 1 {
1237                Some(LeapInfo {
1238                    leap59: r.read_u(1)? != 0,
1239                    leap61: r.read_u(1)? != 0,
1240                    pastleap59: r.read_u(1)? != 0,
1241                    pastleap61: r.read_u(1)? != 0,
1242                })
1243            } else {
1244                r.skip(4)?;
1245                None
1246            };
1247            let ncr_base = r.read_u(33)?;
1248            r.skip(6)?;
1249            let ncr_ext = r.read_u(9)? as u16;
1250            let association_timestamp_seconds = r.read_u(64)?;
1251            let association_timestamp_nanoseconds = r.read_u(32)? as u32;
1252            Ok(SatBody::TimeAssociation(TimeAssociationBody {
1253                association_type,
1254                leap_info,
1255                ncr_base,
1256                ncr_ext,
1257                association_timestamp_seconds,
1258                association_timestamp_nanoseconds,
1259            }))
1260        }
1261        3 => {
1262            let mut plans = Vec::new();
1263            while r.remaining_bits() >= 32 + 4 + 12 {
1264                let beamhopping_time_plan_id = r.read_u(32)? as u32;
1265                r.skip(4)?;
1266                let plan_length = r.read_u(12)? as usize;
1267                let plan_end_bits = r.bits_consumed() + plan_length * 8;
1268                r.skip(6)?;
1269                let time_plan_mode = TimePlanMode::from_u8(r.read_u(2)? as u8);
1270                let time_of_application_base = r.read_u(33)?;
1271                r.skip(6)?;
1272                let time_of_application_ext = r.read_u(9)? as u16;
1273                let cycle_duration_base = r.read_u(33)?;
1274                r.skip(6)?;
1275                let cycle_duration_ext = r.read_u(9)? as u16;
1276                let mode = match time_plan_mode {
1277                    TimePlanMode::DwellOnTime => {
1278                        const MODE0_BITS: usize = 33 + 6 + 9 + 33 + 6 + 9;
1279                        if r.remaining_bits() < MODE0_BITS {
1280                            return Err(Error::BufferTooShort {
1281                                need: MODE0_BITS,
1282                                have: r.remaining_bits(),
1283                                what: "SatSection Beamhopping Mode0",
1284                            });
1285                        }
1286                        let dwell_duration_base = r.read_u(33)?;
1287                        r.skip(6)?;
1288                        let dwell_duration_ext = r.read_u(9)? as u16;
1289                        let on_time_base = r.read_u(33)?;
1290                        r.skip(6)?;
1291                        let on_time_ext = r.read_u(9)? as u16;
1292                        BeamhoppingMode::Mode0 {
1293                            dwell_duration_base,
1294                            dwell_duration_ext,
1295                            on_time_base,
1296                            on_time_ext,
1297                        }
1298                    }
1299                    TimePlanMode::Bitmap => {
1300                        const MODE1_HEADER_BITS: usize = 1 + 15 + 1 + 15;
1301                        if r.remaining_bits() < MODE1_HEADER_BITS {
1302                            return Err(Error::BufferTooShort {
1303                                need: MODE1_HEADER_BITS,
1304                                have: r.remaining_bits(),
1305                                what: "SatSection Beamhopping Mode1 header",
1306                            });
1307                        }
1308                        r.skip(1)?;
1309                        let bit_map_size = r.read_u(15)? as u16;
1310                        r.skip(1)?;
1311                        let current_slot = r.read_u(15)? as u16;
1312                        if r.remaining_bits() < bit_map_size as usize {
1313                            return Err(Error::BufferTooShort {
1314                                need: bit_map_size as usize,
1315                                have: r.remaining_bits(),
1316                                what: "SatSection Beamhopping Mode1 bitmap",
1317                            });
1318                        }
1319                        let mut slot_transmission_on =
1320                            Vec::with_capacity((bit_map_size as usize).min(r.remaining_bits()));
1321                        for _ in 0..bit_map_size {
1322                            slot_transmission_on.push(r.read_u(1)? != 0);
1323                        }
1324                        let total = 1 + 15 + 1 + 15 + bit_map_size as usize;
1325                        r.skip(pad_to_byte(total) as u8)?;
1326                        BeamhoppingMode::Mode1 {
1327                            bit_map_size,
1328                            current_slot,
1329                            slot_transmission_on,
1330                        }
1331                    }
1332                    TimePlanMode::GridRevisitSleep => {
1333                        const MODE2_BITS: usize = 33 + 6 + 9 + 33 + 6 + 9 + 33 + 6 + 9 + 33 + 6 + 9;
1334                        if r.remaining_bits() < MODE2_BITS {
1335                            return Err(Error::BufferTooShort {
1336                                need: MODE2_BITS,
1337                                have: r.remaining_bits(),
1338                                what: "SatSection Beamhopping Mode2",
1339                            });
1340                        }
1341                        let grid_size_base = r.read_u(33)?;
1342                        r.skip(6)?;
1343                        let grid_size_ext = r.read_u(9)? as u16;
1344                        let revisit_duration_base = r.read_u(33)?;
1345                        r.skip(6)?;
1346                        let revisit_duration_ext = r.read_u(9)? as u16;
1347                        let sleep_time_base = r.read_u(33)?;
1348                        r.skip(6)?;
1349                        let sleep_time_ext = r.read_u(9)? as u16;
1350                        let sleep_duration_base = r.read_u(33)?;
1351                        r.skip(6)?;
1352                        let sleep_duration_ext = r.read_u(9)? as u16;
1353                        BeamhoppingMode::Mode2 {
1354                            grid_size_base,
1355                            grid_size_ext,
1356                            revisit_duration_base,
1357                            revisit_duration_ext,
1358                            sleep_time_base,
1359                            sleep_time_ext,
1360                            sleep_duration_base,
1361                            sleep_duration_ext,
1362                        }
1363                    }
1364                    _ => {
1365                        let start_byte = r.bits_consumed().div_ceil(8);
1366                        let end_byte = plan_end_bits / 8;
1367                        let raw = if start_byte < end_byte && end_byte <= data.len() {
1368                            data[start_byte..end_byte].to_vec()
1369                        } else {
1370                            Vec::new()
1371                        };
1372                        BeamhoppingMode::Reserved(raw)
1373                    }
1374                };
1375                r.bit_pos = plan_end_bits;
1376                plans.push(BeamhoppingPlan {
1377                    beamhopping_time_plan_id,
1378                    time_plan_mode,
1379                    time_of_application_base,
1380                    time_of_application_ext,
1381                    cycle_duration_base,
1382                    cycle_duration_ext,
1383                    mode,
1384                });
1385            }
1386            Ok(SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
1387                plans,
1388            }))
1389        }
1390        4 => {
1391            const POS_V3_HEADER_BITS: usize = 4 + 4 + 8 + 7 + 9 + 32;
1392            if r.remaining_bits() < POS_V3_HEADER_BITS {
1393                return Err(Error::BufferTooShort {
1394                    need: POS_V3_HEADER_BITS,
1395                    have: r.remaining_bits(),
1396                    what: "SatSection PositionV3 body header",
1397                });
1398            }
1399            let oem_version_major = r.read_u(4)? as u8;
1400            let oem_version_minor = r.read_u(4)? as u8;
1401            let creation_date_year = r.read_u(8)? as u8;
1402            r.skip(7)?;
1403            let creation_date_day = r.read_u(9)? as u16;
1404            let creation_date_day_fraction = r.read_u(32)? as u32;
1405            let mut satellites = Vec::new();
1406            while r.remaining_bits() >= 24 + 3 + 5 {
1407                let satellite_id = r.read_u(24)? as u32;
1408                r.skip(3)?;
1409                let metadata_flag = r.read_u(1)? != 0;
1410                let usable_start_time_flag = r.read_u(1)? != 0;
1411                let usable_stop_time_flag = r.read_u(1)? != 0;
1412                let ephemeris_accel_flag = r.read_u(1)? != 0;
1413                let covariance_flag = r.read_u(1)? != 0;
1414                let metadata = if metadata_flag {
1415                    const METADATA_FIXED_BITS: usize =
1416                        8 + 7 + 9 + 32 + 8 + 7 + 9 + 32 + 1 + 1 + 3 + 3;
1417                    if r.remaining_bits() < METADATA_FIXED_BITS {
1418                        return Err(Error::BufferTooShort {
1419                            need: METADATA_FIXED_BITS,
1420                            have: r.remaining_bits(),
1421                            what: "SatSection PositionV3 metadata",
1422                        });
1423                    }
1424                    let total_start_time_year = r.read_u(8)? as u8;
1425                    r.skip(7)?;
1426                    let total_start_time_day = r.read_u(9)? as u16;
1427                    let total_start_time_day_fraction = r.read_u(32)? as u32;
1428                    let total_stop_time_year = r.read_u(8)? as u8;
1429                    r.skip(7)?;
1430                    let total_stop_time_day = r.read_u(9)? as u16;
1431                    let total_stop_time_day_fraction = r.read_u(32)? as u32;
1432                    r.skip(1)?;
1433                    let interpolation_flag = r.read_u(1)? != 0;
1434                    let interpolation_type = InterpolationType::from_u8(r.read_u(3)? as u8);
1435                    let interpolation_degree = r.read_u(3)? as u8;
1436                    let usable_start_time = if usable_start_time_flag {
1437                        const USABLE_TIME_BITS: usize = 8 + 7 + 9 + 32;
1438                        if r.remaining_bits() < USABLE_TIME_BITS {
1439                            return Err(Error::BufferTooShort {
1440                                need: USABLE_TIME_BITS,
1441                                have: r.remaining_bits(),
1442                                what: "SatSection PositionV3 usable_start_time",
1443                            });
1444                        }
1445                        let year = r.read_u(8)? as u8;
1446                        r.skip(7)?;
1447                        let day = r.read_u(9)? as u16;
1448                        let day_fraction = r.read_u(32)? as u32;
1449                        Some(UsableTime {
1450                            year,
1451                            day,
1452                            day_fraction,
1453                        })
1454                    } else {
1455                        None
1456                    };
1457                    let usable_stop_time = if usable_stop_time_flag {
1458                        const USABLE_TIME_BITS: usize = 8 + 7 + 9 + 32;
1459                        if r.remaining_bits() < USABLE_TIME_BITS {
1460                            return Err(Error::BufferTooShort {
1461                                need: USABLE_TIME_BITS,
1462                                have: r.remaining_bits(),
1463                                what: "SatSection PositionV3 usable_stop_time",
1464                            });
1465                        }
1466                        let year = r.read_u(8)? as u8;
1467                        r.skip(7)?;
1468                        let day = r.read_u(9)? as u16;
1469                        let day_fraction = r.read_u(32)? as u32;
1470                        Some(UsableTime {
1471                            year,
1472                            day,
1473                            day_fraction,
1474                        })
1475                    } else {
1476                        None
1477                    };
1478                    Some(PositionV3Metadata {
1479                        total_start_time_year,
1480                        total_start_time_day,
1481                        total_start_time_day_fraction,
1482                        total_stop_time_year,
1483                        total_stop_time_day,
1484                        total_stop_time_day_fraction,
1485                        interpolation_flag,
1486                        interpolation_type,
1487                        interpolation_degree,
1488                        usable_start_time,
1489                        usable_stop_time,
1490                    })
1491                } else {
1492                    None
1493                };
1494                let ephemeris_data_count = r.read_u(16)? as u16;
1495                let entry_bits: usize =
1496                    8 + 7 + 9 + 32 + 32 * 6 + if ephemeris_accel_flag { 32 * 3 } else { 0 };
1497                let mut ephemeris_data = Vec::with_capacity(
1498                    (ephemeris_data_count as usize)
1499                        .min(r.remaining_bits().saturating_sub(entry_bits) / entry_bits + 1),
1500                );
1501                for _ in 0..ephemeris_data_count {
1502                    if r.remaining_bits() < entry_bits {
1503                        return Err(Error::BufferTooShort {
1504                            need: entry_bits,
1505                            have: r.remaining_bits(),
1506                            what: "SatSection PositionV3 ephemeris_data entry",
1507                        });
1508                    }
1509                    let epoch_year = r.read_u(8)? as u8;
1510                    r.skip(7)?;
1511                    let epoch_day = r.read_u(9)? as u16;
1512                    let epoch_day_fraction = r.read_u(32)? as u32;
1513                    let ephemeris_x = r.read_u(32)? as u32;
1514                    let ephemeris_y = r.read_u(32)? as u32;
1515                    let ephemeris_z = r.read_u(32)? as u32;
1516                    let ephemeris_x_dot = r.read_u(32)? as u32;
1517                    let ephemeris_y_dot = r.read_u(32)? as u32;
1518                    let ephemeris_z_dot = r.read_u(32)? as u32;
1519                    let acceleration = if ephemeris_accel_flag {
1520                        Some(EphemerisAccel {
1521                            ephemeris_x_ddot: r.read_u(32)? as u32,
1522                            ephemeris_y_ddot: r.read_u(32)? as u32,
1523                            ephemeris_z_ddot: r.read_u(32)? as u32,
1524                        })
1525                    } else {
1526                        None
1527                    };
1528                    ephemeris_data.push(EphemerisData {
1529                        epoch_year,
1530                        epoch_day,
1531                        epoch_day_fraction,
1532                        ephemeris_x,
1533                        ephemeris_y,
1534                        ephemeris_z,
1535                        ephemeris_x_dot,
1536                        ephemeris_y_dot,
1537                        ephemeris_z_dot,
1538                        acceleration,
1539                    });
1540                }
1541                let covariance = if covariance_flag {
1542                    const COV_HEADER_BITS: usize = 8 + 7 + 9 + 32;
1543                    const COV_ELEMENTS_BITS: usize = 21 * 32;
1544                    const COV_BITS: usize = COV_HEADER_BITS + COV_ELEMENTS_BITS;
1545                    if r.remaining_bits() < COV_BITS {
1546                        return Err(Error::BufferTooShort {
1547                            need: COV_BITS,
1548                            have: r.remaining_bits(),
1549                            what: "SatSection PositionV3 covariance",
1550                        });
1551                    }
1552                    let covariance_epoch_year = r.read_u(8)? as u8;
1553                    r.skip(7)?;
1554                    let covariance_epoch_day = r.read_u(9)? as u16;
1555                    let covariance_epoch_day_fraction = r.read_u(32)? as u32;
1556                    let mut covariance_elements = [0u32; 21];
1557                    for elem in &mut covariance_elements {
1558                        *elem = r.read_u(32)? as u32;
1559                    }
1560                    Some(CovarianceData {
1561                        covariance_epoch_year,
1562                        covariance_epoch_day,
1563                        covariance_epoch_day_fraction,
1564                        covariance_elements,
1565                    })
1566                } else {
1567                    None
1568                };
1569                satellites.push(PositionV3Satellite {
1570                    satellite_id,
1571                    usable_start_time_flag,
1572                    usable_stop_time_flag,
1573                    ephemeris_accel_flag,
1574                    covariance_flag,
1575                    metadata,
1576                    ephemeris_data,
1577                    covariance,
1578                });
1579            }
1580            Ok(SatBody::PositionV3(PositionV3Body {
1581                oem_version_major,
1582                oem_version_minor,
1583                creation_date_year,
1584                creation_date_day,
1585                creation_date_day_fraction,
1586                satellites,
1587            }))
1588        }
1589        _ => Ok(SatBody::Raw(data.to_vec())),
1590    }
1591}
1592
1593// ── SatSection ──────────────────────────────────────────────────────────────
1594
1595/// Satellite Access Table section (EN 300 468 §5.2.11.1, Table 11a).
1596///
1597/// The body is typed as [`SatBody`], selected by `satellite_table_id`.
1598/// All body fields are owned numeric values; the section does not borrow
1599/// from the input buffer.
1600#[derive(Debug, Clone, PartialEq, Eq)]
1601#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1602pub struct SatSection {
1603    /// 6-bit discriminant selecting the body structure (see [`SatTableId`]).
1604    pub satellite_table_id: u8,
1605    /// `private_indicator` — byte 1 bit 6 (Table 11a).
1606    pub private_indicator: bool,
1607    /// 10-bit sub_table discriminator.
1608    pub table_count: u16,
1609    /// 5-bit sub_table version number.
1610    pub version_number: u8,
1611    /// When `true`, this sub_table is currently applicable.
1612    pub current_next_indicator: bool,
1613    /// Section number within the sub_table.
1614    pub section_number: u8,
1615    /// Highest section number of the sub_table.
1616    pub last_section_number: u8,
1617    /// Typed body — interpret per `satellite_table_id`.
1618    pub body: SatBody,
1619}
1620
1621impl SatSection {
1622    /// Typed view of `satellite_table_id`, or `None` if reserved (5–63).
1623    #[must_use]
1624    pub fn kind(&self) -> Option<SatTableId> {
1625        SatTableId::try_from(self.satellite_table_id).ok()
1626    }
1627}
1628
1629impl<'a> Parse<'a> for SatSection {
1630    type Error = crate::error::Error;
1631    fn parse(bytes: &'a [u8]) -> Result<Self> {
1632        let min_len = HEADER_LEN + CRC_LEN;
1633        if bytes.len() < min_len {
1634            return Err(Error::BufferTooShort {
1635                need: min_len,
1636                have: bytes.len(),
1637                what: "SatSection",
1638            });
1639        }
1640        if bytes[0] != TABLE_ID {
1641            return Err(Error::UnexpectedTableId {
1642                table_id: bytes[0],
1643                what: "SatSection",
1644                expected: &[TABLE_ID],
1645            });
1646        }
1647        let section_length = (((bytes[1] & 0x0F) as usize) << 8) | bytes[2] as usize;
1648        let total = super::check_section_length(
1649            bytes.len(),
1650            SECTION_LENGTH_PREFIX,
1651            section_length,
1652            HEADER_LEN + CRC_LEN,
1653        )?;
1654        let satellite_table_id = bytes[3] >> 2;
1655        let private_indicator = (bytes[1] & 0x40) != 0;
1656        let table_count = (((bytes[3] & 0x03) as u16) << 8) | bytes[4] as u16;
1657        let version_number = (bytes[5] >> 1) & 0x1F;
1658        let current_next_indicator = bytes[5] & 0x01 != 0;
1659        let section_number = bytes[6];
1660        let last_section_number = bytes[7];
1661        let body_data = &bytes[HEADER_LEN..total - CRC_LEN];
1662        let body = sat_body_parse(satellite_table_id, body_data)?;
1663        Ok(SatSection {
1664            satellite_table_id,
1665            private_indicator,
1666            table_count,
1667            version_number,
1668            current_next_indicator,
1669            section_number,
1670            last_section_number,
1671            body,
1672        })
1673    }
1674}
1675
1676impl Serialize for SatSection {
1677    type Error = crate::error::Error;
1678    fn serialized_len(&self) -> usize {
1679        HEADER_LEN + sat_body_serialized_len(&self.body) + CRC_LEN
1680    }
1681    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
1682        let len = self.serialized_len();
1683        if buf.len() < len {
1684            return Err(Error::OutputBufferTooSmall {
1685                need: len,
1686                have: buf.len(),
1687            });
1688        }
1689        let section_length_usize = len - SECTION_LENGTH_PREFIX;
1690        if section_length_usize > 0x0FFF {
1691            return Err(Error::SectionLengthOverflow {
1692                declared: section_length_usize,
1693                available: 0x0FFF,
1694            });
1695        }
1696        let section_length = section_length_usize as u16;
1697        if let SatBody::PositionV3(ref v3) = self.body {
1698            for sat in &v3.satellites {
1699                if sat.ephemeris_data.len() > u16::MAX as usize {
1700                    return Err(Error::SectionLengthOverflow {
1701                        declared: sat.ephemeris_data.len(),
1702                        available: u16::MAX as usize,
1703                    });
1704                }
1705            }
1706        }
1707        buf[0] = TABLE_ID;
1708        buf[1] = super::SECTION_B1_SSI
1709            | (u8::from(self.private_indicator) << 6)
1710            | super::SECTION_B1_RESERVED_HI
1711            | ((section_length >> 8) as u8 & 0x0F);
1712        buf[2] = (section_length & 0xFF) as u8;
1713        buf[3] = (self.satellite_table_id << 2) | ((self.table_count >> 8) as u8 & 0x03);
1714        buf[4] = (self.table_count & 0xFF) as u8;
1715        buf[5] = 0xC0 | ((self.version_number & 0x1F) << 1) | u8::from(self.current_next_indicator);
1716        buf[6] = self.section_number;
1717        buf[7] = self.last_section_number;
1718        buf[8] = 0x00;
1719        let body_start = HEADER_LEN;
1720        match &self.body {
1721            SatBody::Raw(v) => {
1722                buf[body_start..body_start + v.len()].copy_from_slice(v);
1723            }
1724            _ => {
1725                let body_byte_len = sat_body_serialized_len(&self.body);
1726                for b in &mut buf[body_start..body_start + body_byte_len] {
1727                    *b = 0;
1728                }
1729                let mut writer = BitWriter::new(&mut buf[body_start..body_start + body_byte_len]);
1730                sat_body_write(&self.body, &mut writer)?;
1731            }
1732        }
1733        let body_end = HEADER_LEN + sat_body_serialized_len(&self.body);
1734        let crc = dvb_common::crc32_mpeg2::compute(&buf[..body_end]);
1735        buf[body_end..len].copy_from_slice(&crc.to_be_bytes());
1736        Ok(len)
1737    }
1738}
1739impl<'a> crate::traits::TableDef<'a> for SatSection {
1740    const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
1741    const NAME: &'static str = "SATELLITE_ACCESS";
1742}
1743
1744#[cfg(test)]
1745mod tests {
1746    use super::*;
1747
1748    fn build_sat(stid: u8, table_count: u16, body: &SatBody) -> Vec<u8> {
1749        let sat = SatSection {
1750            satellite_table_id: stid,
1751            private_indicator: true,
1752            table_count,
1753            version_number: 5,
1754            current_next_indicator: true,
1755            section_number: 0,
1756            last_section_number: 0,
1757            body: body.clone(),
1758        };
1759        let mut buf = vec![0u8; sat.serialized_len()];
1760        sat.serialize_into(&mut buf).unwrap();
1761        buf
1762    }
1763
1764    fn build_sat_private_indicator_false(stid: u8, body: &SatBody) -> Vec<u8> {
1765        let sat = SatSection {
1766            satellite_table_id: stid,
1767            private_indicator: false,
1768            table_count: 0,
1769            version_number: 5,
1770            current_next_indicator: true,
1771            section_number: 0,
1772            last_section_number: 0,
1773            body: body.clone(),
1774        };
1775        let mut buf = vec![0u8; sat.serialized_len()];
1776        sat.serialize_into(&mut buf).unwrap();
1777        buf
1778    }
1779
1780    #[test]
1781    fn parse_raw_body() {
1782        let body_data = [0xAA, 0xBB, 0xCC, 0xDD];
1783        let bytes = build_sat(7, 0, &SatBody::Raw(body_data.to_vec()));
1784        let sat = SatSection::parse(&bytes).unwrap();
1785        assert_eq!(sat.satellite_table_id, 7);
1786        assert_eq!(sat.kind(), None);
1787        assert_eq!(sat.body, SatBody::Raw(body_data.to_vec()));
1788    }
1789
1790    #[test]
1791    fn private_indicator_false_round_trip() {
1792        let body = SatBody::TimeAssociation(TimeAssociationBody {
1793            association_type: AssociationType::UtcWithoutLeap,
1794            leap_info: None,
1795            ncr_base: 0,
1796            ncr_ext: 0,
1797            association_timestamp_seconds: 0,
1798            association_timestamp_nanoseconds: 0,
1799        });
1800        let bytes = build_sat_private_indicator_false(2, &body);
1801        let sat = SatSection::parse(&bytes).unwrap();
1802        assert!(!sat.private_indicator);
1803        let mut buf2 = vec![0u8; sat.serialized_len()];
1804        sat.serialize_into(&mut buf2).unwrap();
1805        assert_eq!(
1806            bytes, buf2,
1807            "byte-exact round-trip with private_indicator=false"
1808        );
1809    }
1810
1811    #[test]
1812    fn parse_position_v3_discriminant() {
1813        let body = SatBody::PositionV3(PositionV3Body {
1814            oem_version_major: 1,
1815            oem_version_minor: 0,
1816            creation_date_year: 25,
1817            creation_date_day: 100,
1818            creation_date_day_fraction: 0,
1819            satellites: Vec::new(),
1820        });
1821        let bytes = build_sat(4, 0x1A3, &body);
1822        let sat = SatSection::parse(&bytes).unwrap();
1823        assert_eq!(sat.satellite_table_id, 4);
1824        assert_eq!(sat.kind(), Some(SatTableId::PositionV3));
1825        assert_eq!(sat.table_count, 0x1A3);
1826    }
1827
1828    #[test]
1829    fn time_association_round_trip() {
1830        let body = SatBody::TimeAssociation(TimeAssociationBody {
1831            association_type: AssociationType::UtcWithLeap,
1832            leap_info: Some(LeapInfo {
1833                leap59: true,
1834                leap61: false,
1835                pastleap59: false,
1836                pastleap61: true,
1837            }),
1838            ncr_base: 0x0000_AAAA_AAAA_u64,
1839            ncr_ext: 0x1AA,
1840            association_timestamp_seconds: 0x12345678_9ABCDEF0,
1841            association_timestamp_nanoseconds: 0xDEADBEEF,
1842        });
1843        let bytes = build_sat(2, 0, &body);
1844        let sat = SatSection::parse(&bytes).unwrap();
1845        match &sat.body {
1846            SatBody::TimeAssociation(ta) => {
1847                assert_eq!(ta.association_type, AssociationType::UtcWithLeap);
1848                let li = ta.leap_info.as_ref().unwrap();
1849                assert!(li.leap59);
1850                assert!(!li.leap61);
1851                assert!(!li.pastleap59);
1852                assert!(li.pastleap61);
1853                assert_eq!(ta.ncr_base, 0x0000_AAAA_AAAA);
1854                assert_eq!(ta.ncr_ext, 0x1AA);
1855                assert_eq!(ta.association_timestamp_seconds, 0x12345678_9ABCDEF0);
1856                assert_eq!(ta.association_timestamp_nanoseconds, 0xDEADBEEF);
1857            }
1858            other => panic!("expected TimeAssociation, got {other:?}"),
1859        }
1860        let mut buf2 = vec![0u8; sat.serialized_len()];
1861        sat.serialize_into(&mut buf2).unwrap();
1862        assert_eq!(bytes, buf2, "byte-exact re-serialize");
1863    }
1864
1865    #[test]
1866    fn position_v2_orbital_round_trip() {
1867        let body = SatBody::PositionV2(PositionV2Body {
1868            satellites: vec![PositionV2Satellite {
1869                satellite_id: 0x123456,
1870                position: PositionSystem::Orbital {
1871                    orbital_position: 0x1234,
1872                    west_east_flag: true,
1873                },
1874            }],
1875        });
1876        let bytes = build_sat(0, 0, &body);
1877        let sat = SatSection::parse(&bytes).unwrap();
1878        match &sat.body {
1879            SatBody::PositionV2(pv2) => {
1880                assert_eq!(pv2.satellites.len(), 1);
1881                assert_eq!(pv2.satellites[0].satellite_id, 0x123456);
1882                match &pv2.satellites[0].position {
1883                    PositionSystem::Orbital {
1884                        orbital_position,
1885                        west_east_flag,
1886                    } => {
1887                        assert_eq!(*orbital_position, 0x1234);
1888                        assert!(*west_east_flag);
1889                    }
1890                    other => panic!("expected Orbital, got {other:?}"),
1891                }
1892            }
1893            other => panic!("expected PositionV2, got {other:?}"),
1894        }
1895        let mut buf2 = vec![0u8; sat.serialized_len()];
1896        sat.serialize_into(&mut buf2).unwrap();
1897        assert_eq!(bytes, buf2, "byte-exact re-serialize");
1898    }
1899
1900    #[test]
1901    fn beamhopping_mode0_round_trip() {
1902        let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
1903            plans: vec![BeamhoppingPlan {
1904                beamhopping_time_plan_id: 0xDEADBEEF,
1905                time_plan_mode: TimePlanMode::DwellOnTime,
1906                time_of_application_base: 0x0000_AAAA_AAAA,
1907                time_of_application_ext: 0x100,
1908                cycle_duration_base: 0x0000_5555_5555,
1909                cycle_duration_ext: 0x080,
1910                mode: BeamhoppingMode::Mode0 {
1911                    dwell_duration_base: 0x0000_1111_1111,
1912                    dwell_duration_ext: 0x111,
1913                    on_time_base: 0x0000_2222_2222,
1914                    on_time_ext: 0x222,
1915                },
1916            }],
1917        });
1918        let bytes = build_sat(3, 0, &body);
1919        let sat = SatSection::parse(&bytes).unwrap();
1920        match &sat.body {
1921            SatBody::BeamhoppingTimePlan(bhp) => {
1922                assert_eq!(bhp.plans.len(), 1);
1923                assert_eq!(bhp.plans[0].beamhopping_time_plan_id, 0xDEADBEEF);
1924                assert_eq!(bhp.plans[0].time_plan_mode, TimePlanMode::DwellOnTime);
1925                match &bhp.plans[0].mode {
1926                    BeamhoppingMode::Mode0 {
1927                        dwell_duration_base,
1928                        ..
1929                    } => {
1930                        assert_eq!(*dwell_duration_base, 0x0000_1111_1111);
1931                    }
1932                    other => panic!("expected Mode0, got {other:?}"),
1933                }
1934            }
1935            other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
1936        }
1937        let mut buf2 = vec![0u8; sat.serialized_len()];
1938        sat.serialize_into(&mut buf2).unwrap();
1939        assert_eq!(bytes, buf2, "byte-exact re-serialize");
1940    }
1941
1942    #[test]
1943    fn reserved_discriminant_has_no_kind() {
1944        let bytes = build_sat(7, 0, &SatBody::Raw(Vec::new()));
1945        let sat = SatSection::parse(&bytes).unwrap();
1946        assert_eq!(sat.satellite_table_id, 7);
1947        assert_eq!(sat.kind(), None);
1948    }
1949
1950    #[test]
1951    fn parse_rejects_wrong_tag() {
1952        let mut bytes = build_sat(0, 0, &SatBody::Raw(vec![1, 2, 3]));
1953        bytes[0] = 0x40;
1954        assert!(matches!(
1955            SatSection::parse(&bytes).unwrap_err(),
1956            Error::UnexpectedTableId { table_id: 0x40, .. }
1957        ));
1958    }
1959
1960    #[test]
1961    fn rejects_short_buffer() {
1962        assert!(matches!(
1963            SatSection::parse(&[0x4D, 0xF0]).unwrap_err(),
1964            Error::BufferTooShort {
1965                what: "SatSection",
1966                ..
1967            }
1968        ));
1969    }
1970
1971    #[test]
1972    fn serialize_round_trip_raw() {
1973        let body_data = vec![0x01, 0x02, 0x03, 0x04, 0x05];
1974        let sat = SatSection {
1975            satellite_table_id: 10,
1976            private_indicator: true,
1977            table_count: 0x2FF,
1978            version_number: 5,
1979            current_next_indicator: true,
1980            section_number: 0,
1981            last_section_number: 0,
1982            body: SatBody::Raw(body_data.clone()),
1983        };
1984        let mut buf = vec![0u8; sat.serialized_len()];
1985        sat.serialize_into(&mut buf).unwrap();
1986        let re = SatSection::parse(&buf).unwrap();
1987        assert_eq!(re.body, SatBody::Raw(body_data));
1988        assert_eq!(re.table_count, 0x2FF);
1989    }
1990
1991    #[test]
1992    fn parse_handwritten_sat_raw() {
1993        let mut bytes: Vec<u8> = vec![
1994            0x4D, 0xF0, 0x0E, 0x1C, 0x00, 0xCB, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD,
1995        ];
1996        let crc = dvb_common::crc32_mpeg2::compute(&bytes);
1997        bytes.extend_from_slice(&crc.to_be_bytes());
1998        let sat = SatSection::parse(&bytes).unwrap();
1999        assert_eq!(sat.satellite_table_id, 7);
2000        assert_eq!(sat.table_count, 0);
2001        assert_eq!(sat.version_number, 5);
2002        assert!(sat.current_next_indicator);
2003        match sat.body {
2004            SatBody::Raw(v) => assert_eq!(v, &[0xAA, 0xBB, 0xCC, 0xDD]),
2005            other => panic!("expected Raw, got {other:?}"),
2006        }
2007    }
2008
2009    #[test]
2010    fn beamhopping_multi_plan_round_trip() {
2011        let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
2012            plans: vec![
2013                BeamhoppingPlan {
2014                    beamhopping_time_plan_id: 0x11111111,
2015                    time_plan_mode: TimePlanMode::DwellOnTime,
2016                    time_of_application_base: 0x0000_AAAA_AAAA,
2017                    time_of_application_ext: 0x100,
2018                    cycle_duration_base: 0x0000_5555_5555,
2019                    cycle_duration_ext: 0x080,
2020                    mode: BeamhoppingMode::Mode0 {
2021                        dwell_duration_base: 0x0000_1111_1111,
2022                        dwell_duration_ext: 0x111,
2023                        on_time_base: 0x0000_2222_2222,
2024                        on_time_ext: 0x222,
2025                    },
2026                },
2027                BeamhoppingPlan {
2028                    beamhopping_time_plan_id: 0x22222222,
2029                    time_plan_mode: TimePlanMode::DwellOnTime,
2030                    time_of_application_base: 0x0000_BBBB_BBBB,
2031                    time_of_application_ext: 0x200,
2032                    cycle_duration_base: 0x0000_6666_6666,
2033                    cycle_duration_ext: 0x090,
2034                    mode: BeamhoppingMode::Mode0 {
2035                        dwell_duration_base: 0x0000_3333_3333,
2036                        dwell_duration_ext: 0x333,
2037                        on_time_base: 0x0000_4444_4444,
2038                        on_time_ext: 0x444,
2039                    },
2040                },
2041            ],
2042        });
2043        let bytes = build_sat(3, 0, &body);
2044        let sat = SatSection::parse(&bytes).unwrap();
2045        match &sat.body {
2046            SatBody::BeamhoppingTimePlan(bhp) => {
2047                assert_eq!(bhp.plans.len(), 2);
2048                assert_eq!(bhp.plans[0].beamhopping_time_plan_id, 0x11111111);
2049                assert_eq!(bhp.plans[1].beamhopping_time_plan_id, 0x22222222);
2050            }
2051            other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
2052        }
2053        let mut buf2 = vec![0u8; sat.serialized_len()];
2054        sat.serialize_into(&mut buf2).unwrap();
2055        assert_eq!(bytes, buf2, "byte-exact multi-plan round-trip");
2056    }
2057
2058    #[test]
2059    fn position_v3_one_sat_with_metadata_round_trip() {
2060        let body = SatBody::PositionV3(PositionV3Body {
2061            oem_version_major: 2,
2062            oem_version_minor: 1,
2063            creation_date_year: 26,
2064            creation_date_day: 42,
2065            creation_date_day_fraction: 0,
2066            satellites: vec![PositionV3Satellite {
2067                satellite_id: 0xABCDEF,
2068                usable_start_time_flag: true,
2069                usable_stop_time_flag: false,
2070                ephemeris_accel_flag: false,
2071                covariance_flag: false,
2072                metadata: Some(PositionV3Metadata {
2073                    total_start_time_year: 26,
2074                    total_start_time_day: 1,
2075                    total_start_time_day_fraction: 0,
2076                    total_stop_time_year: 27,
2077                    total_stop_time_day: 100,
2078                    total_stop_time_day_fraction: 0,
2079                    interpolation_flag: true,
2080                    interpolation_type: InterpolationType::Linear,
2081                    interpolation_degree: 2,
2082                    usable_start_time: Some(UsableTime {
2083                        year: 26,
2084                        day: 10,
2085                        day_fraction: 0,
2086                    }),
2087                    usable_stop_time: None,
2088                }),
2089                ephemeris_data: Vec::new(),
2090                covariance: None,
2091            }],
2092        });
2093        let bytes = build_sat(4, 0, &body);
2094        let sat = SatSection::parse(&bytes).unwrap();
2095        match &sat.body {
2096            SatBody::PositionV3(v3) => {
2097                assert_eq!(v3.satellites.len(), 1);
2098                assert_eq!(v3.satellites[0].satellite_id, 0xABCDEF);
2099                let md = v3.satellites[0].metadata.as_ref().unwrap();
2100                assert!(md.interpolation_flag);
2101                assert_eq!(md.interpolation_type, InterpolationType::Linear);
2102                assert_eq!(md.interpolation_degree, 2);
2103                assert!(md.usable_start_time.is_some());
2104            }
2105            other => panic!("expected PositionV3, got {other:?}"),
2106        }
2107        let mut buf2 = vec![0u8; sat.serialized_len()];
2108        sat.serialize_into(&mut buf2).unwrap();
2109        assert_eq!(bytes, buf2, "byte-exact PositionV3 round-trip");
2110    }
2111
2112    #[test]
2113    fn cell_fragment_round_trip() {
2114        let body = SatBody::CellFragment(CellFragmentBody {
2115            fragments: vec![CellFragment {
2116                cell_fragment_id: 0x11223344,
2117                first_occurrence: true,
2118                last_occurrence: false,
2119                center: Some(CellCenter {
2120                    center_latitude: 1000,
2121                    center_longitude: -2000,
2122                    max_distance: 500000,
2123                }),
2124                delivery_system_ids: vec![0x55667788],
2125                new_delivery_systems: vec![NewDeliverySystem {
2126                    new_delivery_system_id: 0xAABBCCDD,
2127                    time_of_application_base: 0x0000_1234_5678,
2128                    time_of_application_ext: 0x100,
2129                }],
2130                obsolescent_delivery_systems: vec![ObsolescentDeliverySystem {
2131                    obsolescent_delivery_system_id: 0xEEFF0011,
2132                    time_of_obsolescence_base: 0x0000_9ABC_DEF0,
2133                    time_of_obsolescence_ext: 0x1FF,
2134                }],
2135            }],
2136        });
2137        let bytes = build_sat(1, 0, &body);
2138        let sat = SatSection::parse(&bytes).unwrap();
2139        match &sat.body {
2140            SatBody::CellFragment(cf) => {
2141                assert_eq!(cf.fragments.len(), 1);
2142                assert_eq!(cf.fragments[0].cell_fragment_id, 0x11223344);
2143                assert!(cf.fragments[0].first_occurrence);
2144                assert!(cf.fragments[0].center.is_some());
2145                assert_eq!(cf.fragments[0].delivery_system_ids.len(), 1);
2146                assert_eq!(cf.fragments[0].new_delivery_systems.len(), 1);
2147                assert_eq!(cf.fragments[0].obsolescent_delivery_systems.len(), 1);
2148            }
2149            other => panic!("expected CellFragment, got {other:?}"),
2150        }
2151        let mut buf2 = vec![0u8; sat.serialized_len()];
2152        sat.serialize_into(&mut buf2).unwrap();
2153        assert_eq!(bytes, buf2, "byte-exact CellFragment round-trip");
2154    }
2155
2156    #[test]
2157    fn beamhopping_mode1_round_trip() {
2158        let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
2159            plans: vec![BeamhoppingPlan {
2160                beamhopping_time_plan_id: 0x12345678,
2161                time_plan_mode: TimePlanMode::Bitmap,
2162                time_of_application_base: 0x0000_AAAA_AAAA,
2163                time_of_application_ext: 0x100,
2164                cycle_duration_base: 0x0000_5555_5555,
2165                cycle_duration_ext: 0x080,
2166                mode: BeamhoppingMode::Mode1 {
2167                    bit_map_size: 8,
2168                    current_slot: 3,
2169                    slot_transmission_on: vec![true, false, true, true, false, false, true, false],
2170                },
2171            }],
2172        });
2173        let bytes = build_sat(3, 0, &body);
2174        let sat = SatSection::parse(&bytes).unwrap();
2175        match &sat.body {
2176            SatBody::BeamhoppingTimePlan(bhp) => {
2177                assert_eq!(bhp.plans.len(), 1);
2178                assert_eq!(bhp.plans[0].time_plan_mode, TimePlanMode::Bitmap);
2179                match &bhp.plans[0].mode {
2180                    BeamhoppingMode::Mode1 {
2181                        bit_map_size,
2182                        current_slot,
2183                        slot_transmission_on,
2184                    } => {
2185                        assert_eq!(*bit_map_size, 8);
2186                        assert_eq!(*current_slot, 3);
2187                        assert_eq!(
2188                            slot_transmission_on,
2189                            &[true, false, true, true, false, false, true, false]
2190                        );
2191                    }
2192                    other => panic!("expected Mode1, got {other:?}"),
2193                }
2194            }
2195            other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
2196        }
2197        let mut buf2 = vec![0u8; sat.serialized_len()];
2198        sat.serialize_into(&mut buf2).unwrap();
2199        assert_eq!(bytes, buf2, "byte-exact Mode1 round-trip");
2200    }
2201
2202    #[test]
2203    fn beamhopping_mode2_round_trip() {
2204        let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
2205            plans: vec![BeamhoppingPlan {
2206                beamhopping_time_plan_id: 0x87654321,
2207                time_plan_mode: TimePlanMode::GridRevisitSleep,
2208                time_of_application_base: 0x0000_BBBB_BBBB,
2209                time_of_application_ext: 0x200,
2210                cycle_duration_base: 0x0000_6666_6666,
2211                cycle_duration_ext: 0x090,
2212                mode: BeamhoppingMode::Mode2 {
2213                    grid_size_base: 0x0000_1111_1111,
2214                    grid_size_ext: 0x111,
2215                    revisit_duration_base: 0x0000_2222_2222,
2216                    revisit_duration_ext: 0x222,
2217                    sleep_time_base: 0x0000_3333_3333,
2218                    sleep_time_ext: 0x333,
2219                    sleep_duration_base: 0x0000_4444_4444,
2220                    sleep_duration_ext: 0x444,
2221                },
2222            }],
2223        });
2224        let bytes = build_sat(3, 0, &body);
2225        let sat = SatSection::parse(&bytes).unwrap();
2226        match &sat.body {
2227            SatBody::BeamhoppingTimePlan(bhp) => {
2228                assert_eq!(bhp.plans.len(), 1);
2229                assert_eq!(bhp.plans[0].time_plan_mode, TimePlanMode::GridRevisitSleep);
2230                match &bhp.plans[0].mode {
2231                    BeamhoppingMode::Mode2 { grid_size_base, .. } => {
2232                        assert_eq!(*grid_size_base, 0x0000_1111_1111);
2233                    }
2234                    other => panic!("expected Mode2, got {other:?}"),
2235                }
2236            }
2237            other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
2238        }
2239        let mut buf2 = vec![0u8; sat.serialized_len()];
2240        sat.serialize_into(&mut buf2).unwrap();
2241        assert_eq!(bytes, buf2, "byte-exact Mode2 round-trip");
2242    }
2243
2244    #[test]
2245    fn beamhopping_reserved_mode_round_trip() {
2246        let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
2247            plans: vec![
2248                BeamhoppingPlan {
2249                    beamhopping_time_plan_id: 0x11111111,
2250                    time_plan_mode: TimePlanMode::DwellOnTime,
2251                    time_of_application_base: 0x0000_AAAA_AAAA,
2252                    time_of_application_ext: 0x100,
2253                    cycle_duration_base: 0x0000_5555_5555,
2254                    cycle_duration_ext: 0x080,
2255                    mode: BeamhoppingMode::Mode0 {
2256                        dwell_duration_base: 0x0000_1111_1111,
2257                        dwell_duration_ext: 0x111,
2258                        on_time_base: 0x0000_2222_2222,
2259                        on_time_ext: 0x222,
2260                    },
2261                },
2262                BeamhoppingPlan {
2263                    beamhopping_time_plan_id: 0x22222222,
2264                    time_plan_mode: TimePlanMode::Reserved(3),
2265                    time_of_application_base: 0x0000_CCCC_CCCC,
2266                    time_of_application_ext: 0x300,
2267                    cycle_duration_base: 0x0000_DDDD_DDDD,
2268                    cycle_duration_ext: 0x400,
2269                    mode: BeamhoppingMode::Reserved(vec![0xAA, 0xBB, 0xCC]),
2270                },
2271            ],
2272        });
2273        let bytes = build_sat(3, 0, &body);
2274        let sat = SatSection::parse(&bytes).unwrap();
2275        match &sat.body {
2276            SatBody::BeamhoppingTimePlan(bhp) => {
2277                assert_eq!(bhp.plans.len(), 2);
2278                assert_eq!(bhp.plans[0].time_plan_mode, TimePlanMode::DwellOnTime);
2279                assert_eq!(bhp.plans[1].time_plan_mode, TimePlanMode::Reserved(3));
2280                match &bhp.plans[1].mode {
2281                    BeamhoppingMode::Reserved(v) => {
2282                        assert_eq!(v, &[0xAA, 0xBB, 0xCC]);
2283                    }
2284                    other => panic!("expected Reserved, got {other:?}"),
2285                }
2286            }
2287            other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
2288        }
2289        let mut buf2 = vec![0u8; sat.serialized_len()];
2290        sat.serialize_into(&mut buf2).unwrap();
2291        assert_eq!(bytes, buf2, "byte-exact Reserved mode round-trip");
2292    }
2293
2294    #[test]
2295    fn cell_fragment_truncated_dsid_count() {
2296        let body = SatBody::CellFragment(CellFragmentBody {
2297            fragments: vec![CellFragment {
2298                cell_fragment_id: 1,
2299                first_occurrence: false,
2300                last_occurrence: false,
2301                center: None,
2302                delivery_system_ids: vec![0x11111111],
2303                new_delivery_systems: Vec::new(),
2304                obsolescent_delivery_systems: Vec::new(),
2305            }],
2306        });
2307        let bytes = build_sat(1, 0, &body);
2308        let sat = SatSection::parse(&bytes).unwrap();
2309        let mut buf2 = vec![0u8; sat.serialized_len()];
2310        sat.serialize_into(&mut buf2).unwrap();
2311        assert_eq!(bytes, buf2);
2312
2313        let corrupt_sat = SatSection {
2314            satellite_table_id: 1,
2315            private_indicator: true,
2316            table_count: 0,
2317            version_number: 5,
2318            current_next_indicator: true,
2319            section_number: 0,
2320            last_section_number: 0,
2321            body: SatBody::CellFragment(CellFragmentBody {
2322                fragments: vec![CellFragment {
2323                    cell_fragment_id: 1,
2324                    first_occurrence: false,
2325                    last_occurrence: false,
2326                    center: None,
2327                    delivery_system_ids: vec![0x11111111; 50],
2328                    new_delivery_systems: Vec::new(),
2329                    obsolescent_delivery_systems: Vec::new(),
2330                }],
2331            }),
2332        };
2333        let mut corrupt_buf = vec![0u8; corrupt_sat.serialized_len()];
2334        corrupt_sat.serialize_into(&mut corrupt_buf).unwrap();
2335        let section_length = (corrupt_buf.len() - SECTION_LENGTH_PREFIX) as u16;
2336        corrupt_buf[1] = 0x80 | 0x40 | 0x30 | ((section_length >> 8) as u8 & 0x0F);
2337        corrupt_buf[2] = (section_length & 0xFF) as u8;
2338        let crc_end = corrupt_buf.len();
2339        let crc = dvb_common::crc32_mpeg2::compute(&corrupt_buf[..crc_end - CRC_LEN]);
2340        corrupt_buf[crc_end - CRC_LEN..crc_end].copy_from_slice(&crc.to_be_bytes());
2341        let original_len = corrupt_buf.len();
2342        corrupt_buf.truncate(original_len - 100);
2343        let sl = (corrupt_buf.len() - SECTION_LENGTH_PREFIX) as u16;
2344        corrupt_buf[1] = (corrupt_buf[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
2345        corrupt_buf[2] = (sl & 0xFF) as u8;
2346        let crc_end = corrupt_buf.len();
2347        let crc2 = dvb_common::crc32_mpeg2::compute(&corrupt_buf[..crc_end - CRC_LEN]);
2348        corrupt_buf[crc_end - CRC_LEN..crc_end].copy_from_slice(&crc2.to_be_bytes());
2349        assert!(SatSection::parse(&corrupt_buf).is_err());
2350    }
2351
2352    #[test]
2353    fn beamhopping_mode1_truncated_bit_map_size() {
2354        let corrupt_sat = SatSection {
2355            satellite_table_id: 3,
2356            private_indicator: true,
2357            table_count: 0,
2358            version_number: 5,
2359            current_next_indicator: true,
2360            section_number: 0,
2361            last_section_number: 0,
2362            body: SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
2363                plans: vec![BeamhoppingPlan {
2364                    beamhopping_time_plan_id: 1,
2365                    time_plan_mode: TimePlanMode::Bitmap,
2366                    time_of_application_base: 0,
2367                    time_of_application_ext: 0,
2368                    cycle_duration_base: 0,
2369                    cycle_duration_ext: 0,
2370                    mode: BeamhoppingMode::Mode1 {
2371                        bit_map_size: 200,
2372                        current_slot: 0,
2373                        slot_transmission_on: vec![true; 200],
2374                    },
2375                }],
2376            }),
2377        };
2378        let mut corrupt_buf = vec![0u8; corrupt_sat.serialized_len()];
2379        corrupt_sat.serialize_into(&mut corrupt_buf).unwrap();
2380        let original_len = corrupt_buf.len();
2381        let truncate_at = HEADER_LEN + 20;
2382        assert!(
2383            truncate_at + CRC_LEN < original_len,
2384            "fixture must be large enough to truncate meaningfully"
2385        );
2386        {
2387            corrupt_buf.truncate(truncate_at + CRC_LEN);
2388            let sl = (corrupt_buf.len() - SECTION_LENGTH_PREFIX) as u16;
2389            corrupt_buf[1] = (corrupt_buf[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
2390            corrupt_buf[2] = (sl & 0xFF) as u8;
2391            let crc_end = corrupt_buf.len();
2392            let crc = dvb_common::crc32_mpeg2::compute(&corrupt_buf[..crc_end - CRC_LEN]);
2393            corrupt_buf[crc_end - CRC_LEN..crc_end].copy_from_slice(&crc.to_be_bytes());
2394            assert!(SatSection::parse(&corrupt_buf).is_err());
2395        }
2396    }
2397
2398    #[test]
2399    fn position_v3_truncated_ephemeris_data_count() {
2400        let corrupt_sat = SatSection {
2401            satellite_table_id: 4,
2402            private_indicator: true,
2403            table_count: 0,
2404            version_number: 5,
2405            current_next_indicator: true,
2406            section_number: 0,
2407            last_section_number: 0,
2408            body: SatBody::PositionV3(PositionV3Body {
2409                oem_version_major: 1,
2410                oem_version_minor: 0,
2411                creation_date_year: 25,
2412                creation_date_day: 1,
2413                creation_date_day_fraction: 0,
2414                satellites: vec![PositionV3Satellite {
2415                    satellite_id: 1,
2416                    usable_start_time_flag: false,
2417                    usable_stop_time_flag: false,
2418                    ephemeris_accel_flag: false,
2419                    covariance_flag: false,
2420                    metadata: None,
2421                    ephemeris_data: vec![
2422                        EphemerisData {
2423                            epoch_year: 25,
2424                            epoch_day: 1,
2425                            epoch_day_fraction: 0,
2426                            ephemeris_x: 0,
2427                            ephemeris_y: 0,
2428                            ephemeris_z: 0,
2429                            ephemeris_x_dot: 0,
2430                            ephemeris_y_dot: 0,
2431                            ephemeris_z_dot: 0,
2432                            acceleration: None,
2433                        };
2434                        5
2435                    ],
2436                    covariance: None,
2437                }],
2438            }),
2439        };
2440        let mut corrupt_buf = vec![0u8; corrupt_sat.serialized_len()];
2441        corrupt_sat.serialize_into(&mut corrupt_buf).unwrap();
2442        let original_len = corrupt_buf.len();
2443        let truncate_at = HEADER_LEN + 30;
2444        assert!(
2445            truncate_at + CRC_LEN < original_len,
2446            "fixture must be large enough to truncate meaningfully"
2447        );
2448        {
2449            corrupt_buf.truncate(truncate_at + CRC_LEN);
2450            let sl = (corrupt_buf.len() - SECTION_LENGTH_PREFIX) as u16;
2451            corrupt_buf[1] = (corrupt_buf[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
2452            corrupt_buf[2] = (sl & 0xFF) as u8;
2453            let crc_end = corrupt_buf.len();
2454            let crc = dvb_common::crc32_mpeg2::compute(&corrupt_buf[..crc_end - CRC_LEN]);
2455            corrupt_buf[crc_end - CRC_LEN..crc_end].copy_from_slice(&crc.to_be_bytes());
2456            assert!(SatSection::parse(&corrupt_buf).is_err());
2457        }
2458    }
2459
2460    #[test]
2461    fn hand_byte_time_association() {
2462        let body = SatBody::TimeAssociation(TimeAssociationBody {
2463            association_type: AssociationType::UtcWithoutLeap,
2464            leap_info: None,
2465            ncr_base: 0x0000_AAAA_AAAA_u64,
2466            ncr_ext: 0x1AA,
2467            association_timestamp_seconds: 0,
2468            association_timestamp_nanoseconds: 0,
2469        });
2470        let bytes = build_sat(2, 0, &body);
2471        let sat = SatSection::parse(&bytes).unwrap();
2472        assert_eq!(sat.satellite_table_id, 2);
2473        match &sat.body {
2474            SatBody::TimeAssociation(ta) => {
2475                assert_eq!(ta.association_type, AssociationType::UtcWithoutLeap);
2476                assert_eq!(ta.ncr_base, 0x0000_AAAA_AAAA);
2477                assert_eq!(ta.ncr_ext, 0x1AA);
2478            }
2479            other => panic!("expected TimeAssociation, got {other:?}"),
2480        }
2481        assert_eq!((bytes[1] >> 6) & 1, 1);
2482        assert_eq!((bytes[3] >> 2) & 0x3F, 2);
2483    }
2484
2485    #[test]
2486    fn hand_byte_position_v2_orbital() {
2487        let body = SatBody::PositionV2(PositionV2Body {
2488            satellites: vec![PositionV2Satellite {
2489                satellite_id: 0x010203,
2490                position: PositionSystem::Orbital {
2491                    orbital_position: 0x1920,
2492                    west_east_flag: true,
2493                },
2494            }],
2495        });
2496        let bytes = build_sat(0, 0, &body);
2497        let sat = SatSection::parse(&bytes).unwrap();
2498        match &sat.body {
2499            SatBody::PositionV2(pv2) => {
2500                assert_eq!(pv2.satellites[0].satellite_id, 0x010203);
2501                match &pv2.satellites[0].position {
2502                    PositionSystem::Orbital {
2503                        orbital_position, ..
2504                    } => {
2505                        assert_eq!(*orbital_position, 0x1920);
2506                    }
2507                    other => panic!("expected Orbital, got {other:?}"),
2508                }
2509            }
2510            other => panic!("expected PositionV2, got {other:?}"),
2511        }
2512        assert_eq!((bytes[3] >> 2) & 0x3F, 0);
2513    }
2514
2515    #[test]
2516    fn hand_byte_cell_fragment() {
2517        let body = SatBody::CellFragment(CellFragmentBody {
2518            fragments: vec![CellFragment {
2519                cell_fragment_id: 0xAABBCCDD,
2520                first_occurrence: false,
2521                last_occurrence: true,
2522                center: None,
2523                delivery_system_ids: Vec::new(),
2524                new_delivery_systems: Vec::new(),
2525                obsolescent_delivery_systems: Vec::new(),
2526            }],
2527        });
2528        let bytes = build_sat(1, 0, &body);
2529        let sat = SatSection::parse(&bytes).unwrap();
2530        match &sat.body {
2531            SatBody::CellFragment(cf) => {
2532                assert_eq!(cf.fragments[0].cell_fragment_id, 0xAABBCCDD);
2533                assert!(cf.fragments[0].last_occurrence);
2534            }
2535            other => panic!("expected CellFragment, got {other:?}"),
2536        }
2537        assert_eq!((bytes[3] >> 2) & 0x3F, 1);
2538    }
2539
2540    #[test]
2541    fn hand_byte_beamhopping_mode0() {
2542        let body = SatBody::BeamhoppingTimePlan(BeamhoppingTimePlanBody {
2543            plans: vec![BeamhoppingPlan {
2544                beamhopping_time_plan_id: 0xDEADBEEF,
2545                time_plan_mode: TimePlanMode::DwellOnTime,
2546                time_of_application_base: 0,
2547                time_of_application_ext: 0,
2548                cycle_duration_base: 0,
2549                cycle_duration_ext: 0,
2550                mode: BeamhoppingMode::Mode0 {
2551                    dwell_duration_base: 0,
2552                    dwell_duration_ext: 0,
2553                    on_time_base: 0,
2554                    on_time_ext: 0,
2555                },
2556            }],
2557        });
2558        let bytes = build_sat(3, 0, &body);
2559        let sat = SatSection::parse(&bytes).unwrap();
2560        match &sat.body {
2561            SatBody::BeamhoppingTimePlan(bhp) => {
2562                assert_eq!(bhp.plans[0].beamhopping_time_plan_id, 0xDEADBEEF);
2563                assert_eq!(bhp.plans[0].time_plan_mode, TimePlanMode::DwellOnTime);
2564            }
2565            other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
2566        }
2567        assert_eq!((bytes[3] >> 2) & 0x3F, 3);
2568    }
2569
2570    #[test]
2571    fn hand_byte_position_v3() {
2572        let body = SatBody::PositionV3(PositionV3Body {
2573            oem_version_major: 1,
2574            oem_version_minor: 2,
2575            creation_date_year: 26,
2576            creation_date_day: 42,
2577            creation_date_day_fraction: 0,
2578            satellites: Vec::new(),
2579        });
2580        let bytes = build_sat(4, 0, &body);
2581        let sat = SatSection::parse(&bytes).unwrap();
2582        match &sat.body {
2583            SatBody::PositionV3(v3) => {
2584                assert_eq!(v3.oem_version_major, 1);
2585                assert_eq!(v3.oem_version_minor, 2);
2586            }
2587            other => panic!("expected PositionV3, got {other:?}"),
2588        }
2589        assert_eq!((bytes[3] >> 2) & 0x3F, 4);
2590    }
2591
2592    // ── Hand-built byte-literal anchor tests ──────────────────────────────────
2593    // Each constructs a wire byte array by hand (no serializer) and verifies
2594    // that the bit-packed parser maps fields to the expected bit positions.
2595    // Re-serialization must then produce byte-identical output.
2596
2597    fn crc_section(bytes: &[u8]) -> Vec<u8> {
2598        let mut v = bytes.to_vec();
2599        let crc = dvb_common::crc32_mpeg2::compute(&v);
2600        v.extend_from_slice(&crc.to_be_bytes());
2601        v
2602    }
2603
2604    #[test]
2605    fn hand_built_time_association_anchor() {
2606        // section_length = body_len(19) + 10 = 29 = 0x001D
2607        // Bit breakdown in body (association_type=0, ncr_base=1, all else 0):
2608        //   [0:3]   association_type(4)=0
2609        //   [4:7]   reserved/ncr_leap(4)=0
2610        //   [8:40]  ncr_base(33)=1    →  LSB lands at bit 40 → byte 5 bit 7
2611        //   [41:46] skip(6)
2612        //   [47:55] ncr_ext(9)=0
2613        //   [56:119] timestamp_seconds(64)=0
2614        //   [120:151] timestamp_nanoseconds(32)=0
2615        let bytes = crc_section(&[
2616            0x4D, 0xF0, 0x1D, 0x08, 0x00, 0xCB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2617            0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2618        ]);
2619        let sat = SatSection::parse(&bytes).unwrap();
2620        assert_eq!(sat.satellite_table_id, 2);
2621        match &sat.body {
2622            SatBody::TimeAssociation(ta) => {
2623                assert_eq!(ta.association_type, AssociationType::UtcWithoutLeap);
2624                assert_eq!(ta.ncr_base, 1);
2625                assert_eq!(ta.ncr_ext, 0);
2626                assert_eq!(ta.association_timestamp_seconds, 0);
2627                assert_eq!(ta.association_timestamp_nanoseconds, 0);
2628            }
2629            other => panic!("expected TimeAssociation, got {other:?}"),
2630        }
2631        let mut buf = vec![0u8; sat.serialized_len()];
2632        sat.serialize_into(&mut buf).unwrap();
2633        assert_eq!(buf, bytes, "byte-identical re-serialize");
2634    }
2635
2636    #[test]
2637    fn hand_built_position_v2_orbital_anchor() {
2638        // section_length = body_len(7) + 10 = 17 = 0x0011
2639        // Bit breakdown in body (one satellite, orbital position):
2640        //   [0:23]   satellite_id(24)=0x010203
2641        //   [24:30]  skip(7)
2642        //   [31]     position_system(1)=0 → orbital
2643        //   [32:47]  orbital_position(16)=0x1234
2644        //   [48]     west_east_flag(1)=1
2645        //   [49:55]  skip(7)
2646        let bytes = crc_section(&[
2647            0x4D, 0xF0, 0x11, 0x00, 0x00, 0xCB, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x00, 0x12,
2648            0x34, 0x80,
2649        ]);
2650        let sat = SatSection::parse(&bytes).unwrap();
2651        assert_eq!(sat.satellite_table_id, 0);
2652        match &sat.body {
2653            SatBody::PositionV2(pv2) => {
2654                assert_eq!(pv2.satellites.len(), 1);
2655                let s = &pv2.satellites[0];
2656                assert_eq!(s.satellite_id, 0x010203);
2657                match &s.position {
2658                    PositionSystem::Orbital {
2659                        orbital_position,
2660                        west_east_flag,
2661                    } => {
2662                        assert_eq!(*orbital_position, 0x1234);
2663                        assert!(*west_east_flag);
2664                    }
2665                    other => panic!("expected Orbital, got {other:?}"),
2666                }
2667            }
2668            other => panic!("expected PositionV2, got {other:?}"),
2669        }
2670        let mut buf = vec![0u8; sat.serialized_len()];
2671        sat.serialize_into(&mut buf).unwrap();
2672        assert_eq!(buf, bytes, "byte-identical re-serialize");
2673    }
2674
2675    #[test]
2676    fn hand_built_cell_fragment_anchor() {
2677        // section_length = body_len(10) + 10 = 20 = 0x0014
2678        // Bit breakdown in body (one fragment, no center, empty lists):
2679        //   [0:31]   cell_fragment_id(32)=0xAABBCCDD
2680        //   [32]     first_occurrence(1)=0
2681        //   [33]     last_occurrence(1)=1
2682        //   [34:37]  skip(4)
2683        //   [38:47]  delivery_system_ids_count(10)=0
2684        //   [48:53]  skip(6)
2685        //   [54:63]  new_delivery_systems_count(10)=0
2686        //   [64:69]  skip(6)
2687        //   [70:79]  obsolescent_delivery_systems_count(10)=0
2688        let bytes = crc_section(&[
2689            0x4D, 0xF0, 0x14, 0x04, 0x00, 0xCB, 0x00, 0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0x40,
2690            0x00, 0x00, 0x00, 0x00, 0x00,
2691        ]);
2692        let sat = SatSection::parse(&bytes).unwrap();
2693        assert_eq!(sat.satellite_table_id, 1);
2694        match &sat.body {
2695            SatBody::CellFragment(cf) => {
2696                assert_eq!(cf.fragments.len(), 1);
2697                let f = &cf.fragments[0];
2698                assert_eq!(f.cell_fragment_id, 0xAABBCCDD);
2699                assert!(!f.first_occurrence);
2700                assert!(f.last_occurrence);
2701                assert!(f.center.is_none());
2702                assert!(f.delivery_system_ids.is_empty());
2703                assert!(f.new_delivery_systems.is_empty());
2704                assert!(f.obsolescent_delivery_systems.is_empty());
2705            }
2706            other => panic!("expected CellFragment, got {other:?}"),
2707        }
2708        let mut buf = vec![0u8; sat.serialized_len()];
2709        sat.serialize_into(&mut buf).unwrap();
2710        assert_eq!(buf, bytes, "byte-identical re-serialize");
2711    }
2712
2713    #[test]
2714    fn hand_built_beamhopping_mode0_anchor() {
2715        // section_length = body_len(31) + 10 = 41 = 0x0029
2716        // Bit breakdown in body (one plan, Mode0, all times zero):
2717        //   [0:31]   plan_id(32)=0xDEADBEEF
2718        //   [32:35]  skip(4)
2719        //   [36:47]  plan_length(12)=25 → 0x019
2720        //   [48:53]  skip(6)
2721        //   [54:55]  plan_mode(2)=0
2722        //   [56:247] all zero (times, durations)
2723        let bytes = crc_section(&[
2724            0x4D, 0xF0, 0x29, 0x0C, 0x00, 0xCB, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x00,
2725            0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2726            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
2727        ]);
2728        let sat = SatSection::parse(&bytes).unwrap();
2729        assert_eq!(sat.satellite_table_id, 3);
2730        match &sat.body {
2731            SatBody::BeamhoppingTimePlan(bhp) => {
2732                assert_eq!(bhp.plans.len(), 1);
2733                let p = &bhp.plans[0];
2734                assert_eq!(p.beamhopping_time_plan_id, 0xDEADBEEF);
2735                assert_eq!(p.time_plan_mode, TimePlanMode::DwellOnTime);
2736                assert_eq!(p.time_of_application_base, 0);
2737                assert_eq!(p.cycle_duration_base, 0);
2738                match &p.mode {
2739                    BeamhoppingMode::Mode0 {
2740                        dwell_duration_base,
2741                        dwell_duration_ext,
2742                        on_time_base,
2743                        on_time_ext,
2744                    } => {
2745                        assert_eq!(*dwell_duration_base, 0);
2746                        assert_eq!(*dwell_duration_ext, 0);
2747                        assert_eq!(*on_time_base, 0);
2748                        assert_eq!(*on_time_ext, 0);
2749                    }
2750                    other => panic!("expected Mode0, got {other:?}"),
2751                }
2752            }
2753            other => panic!("expected BeamhoppingTimePlan, got {other:?}"),
2754        }
2755        let mut buf = vec![0u8; sat.serialized_len()];
2756        sat.serialize_into(&mut buf).unwrap();
2757        assert_eq!(buf, bytes, "byte-identical re-serialize");
2758    }
2759
2760    #[test]
2761    fn hand_built_position_v3_anchor() {
2762        // section_length = body_len(8) + 10 = 18 = 0x0012
2763        // Bit breakdown in body (empty satellites):
2764        //   [0:3]   oem_version_major(4)=1
2765        //   [4:7]   oem_version_minor(4)=2        → byte 0 = 0x12
2766        //   [8:15]  creation_date_year(8)=26       → byte 1 = 0x1A
2767        //   [16:22] skip(7)
2768        //   [23:31] creation_date_day(9)=42        → byte 2 bit 0=0, byte 3 = 0x2A
2769        //   [32:63] creation_date_day_fraction(32)=0 → bytes 4-7
2770        let bytes = crc_section(&[
2771            0x4D, 0xF0, 0x12, 0x10, 0x00, 0xCB, 0x00, 0x00, 0x00, 0x12, 0x1A, 0x00, 0x2A, 0x00,
2772            0x00, 0x00, 0x00,
2773        ]);
2774        let sat = SatSection::parse(&bytes).unwrap();
2775        assert_eq!(sat.satellite_table_id, 4);
2776        match &sat.body {
2777            SatBody::PositionV3(v3) => {
2778                assert_eq!(v3.oem_version_major, 1);
2779                assert_eq!(v3.oem_version_minor, 2);
2780                assert_eq!(v3.creation_date_year, 26);
2781                assert_eq!(v3.creation_date_day, 42);
2782                assert_eq!(v3.creation_date_day_fraction, 0);
2783                assert!(v3.satellites.is_empty());
2784            }
2785            other => panic!("expected PositionV3, got {other:?}"),
2786        }
2787        let mut buf = vec![0u8; sat.serialized_len()];
2788        sat.serialize_into(&mut buf).unwrap();
2789        assert_eq!(buf, bytes, "byte-identical re-serialize");
2790    }
2791
2792    #[test]
2793    fn parse_rejects_truncated_time_association_body() {
2794        let body = SatBody::TimeAssociation(TimeAssociationBody {
2795            association_type: AssociationType::UtcWithoutLeap,
2796            leap_info: None,
2797            ncr_base: 0,
2798            ncr_ext: 0,
2799            association_timestamp_seconds: 0,
2800            association_timestamp_nanoseconds: 0,
2801        });
2802        let bytes = build_sat(2, 0, &body);
2803        let sat = SatSection::parse(&bytes).unwrap();
2804        let mut buf = vec![0u8; sat.serialized_len()];
2805        sat.serialize_into(&mut buf).unwrap();
2806        buf.truncate(HEADER_LEN + 4 + CRC_LEN);
2807        let sl = (buf.len() - SECTION_LENGTH_PREFIX) as u16;
2808        buf[1] = (buf[1] & 0xF0) | ((sl >> 8) as u8 & 0x0F);
2809        buf[2] = (sl & 0xFF) as u8;
2810        let crc_end = buf.len();
2811        let crc = dvb_common::crc32_mpeg2::compute(&buf[..crc_end - CRC_LEN]);
2812        buf[crc_end - CRC_LEN..crc_end].copy_from_slice(&crc.to_be_bytes());
2813        assert!(SatSection::parse(&buf).is_err());
2814    }
2815
2816    #[test]
2817    fn sat_table_id_wire_to_name() {
2818        let stid = SatTableId::try_from(0u8).unwrap();
2819        assert_eq!(stid, SatTableId::PositionV2);
2820        let stid = SatTableId::try_from(2u8).unwrap();
2821        assert_eq!(stid, SatTableId::TimeAssociation);
2822        let stid = SatTableId::try_from(4u8).unwrap();
2823        assert_eq!(stid, SatTableId::PositionV3);
2824    }
2825
2826    #[test]
2827    fn association_type_wire_to_name() {
2828        assert_eq!(
2829            AssociationType::from_u8(0).name(),
2830            "UTC without leap second"
2831        );
2832        assert_eq!(AssociationType::from_u8(1).name(), "UTC with leap second");
2833    }
2834
2835    #[test]
2836    fn interpolation_type_wire_to_name() {
2837        assert_eq!(InterpolationType::from_u8(1).name(), "Linear");
2838        assert_eq!(InterpolationType::from_u8(2).name(), "Lagrange");
2839        assert_eq!(InterpolationType::from_u8(4).name(), "Hermite");
2840        assert_eq!(InterpolationType::from_u8(0).name(), "Reserved");
2841    }
2842
2843    #[test]
2844    fn time_plan_mode_full_range_round_trip() {
2845        for v in 0u8..=0x03 {
2846            let tpm = TimePlanMode::from_u8(v);
2847            assert_eq!(tpm.to_u8(), v, "TimePlanMode round-trip failed for {v}");
2848        }
2849    }
2850
2851    #[test]
2852    fn time_plan_mode_known_values() {
2853        assert_eq!(TimePlanMode::from_u8(0), TimePlanMode::DwellOnTime);
2854        assert_eq!(TimePlanMode::from_u8(1), TimePlanMode::Bitmap);
2855        assert_eq!(TimePlanMode::from_u8(2), TimePlanMode::GridRevisitSleep);
2856        assert_eq!(TimePlanMode::from_u8(3), TimePlanMode::Reserved(3));
2857        assert_eq!(TimePlanMode::DwellOnTime.name(), "dwell/on-time");
2858        assert_eq!(TimePlanMode::Bitmap.name(), "bitmap");
2859        assert_eq!(TimePlanMode::GridRevisitSleep.name(), "grid/revisit/sleep");
2860        assert_eq!(TimePlanMode::Reserved(3).name(), "reserved");
2861    }
2862}