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