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