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