Skip to main content

dvb_si/tables/
sat.rs

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