Skip to main content

dvb_t2mi/payload/l1/
post.rs

1//! L1-post signalling — EN 302 755 §7.2.3 (Figures 27 and 28).
2//!
3//! L1-post consists of:
4//! - **Configurable** part (Figure 27, `L1PostConfigurable`) — stable for
5//!   one super-frame.
6//! - **Dynamic** part (Figure 28, `L1PostDynamic`) — may change each frame.
7//! - Optional **Extension** blocks (Table 37, `L1ExtBlock`).
8//!
9//! As carried in T2-MI (ETSI TS 102 773 §5.2.4 Table 2), each section is
10//! prefixed with a 16-bit bit-length and zero-padded to a byte boundary.
11//!
12//! Parsing uses [`broadcast_common::bits::BitReader`] / [`broadcast_common::bits::BitWriter`] (MSB-first).
13
14use alloc::vec;
15use alloc::vec::Vec;
16use broadcast_common::bits::{BitReader, BitWriter};
17
18use super::enums::{
19    AuxStreamType, PlpCodeRate, PlpFecType, PlpMode, PlpModulation, PlpPayloadType, PlpType,
20};
21
22// ── field widths: Configurable L1-post (Figure 27) ───────────────────────────
23
24const SUB_SLICES_PER_FRAME_BITS: u32 = 15;
25const NUM_PLP_BITS: u32 = 8;
26const NUM_AUX_BITS: u32 = 4;
27const AUX_CONFIG_RFU_BITS: u32 = 8;
28// RF loop per entry
29const RF_IDX_BITS: u32 = 3;
30const FREQUENCY_BITS: u32 = 32;
31// FEF block
32const FEF_TYPE_BITS: u32 = 4;
33const FEF_LENGTH_BITS: u32 = 22;
34const FEF_INTERVAL_BITS: u32 = 8;
35// PLP loop per entry
36const PLP_ID_BITS: u32 = 8;
37const PLP_TYPE_BITS: u32 = 3;
38const PLP_PAYLOAD_TYPE_BITS: u32 = 5;
39const FF_FLAG_BITS: u32 = 1;
40const FIRST_RF_IDX_BITS: u32 = 3;
41const FIRST_FRAME_IDX_BITS: u32 = 8;
42const PLP_GROUP_ID_BITS: u32 = 8;
43const PLP_COD_BITS: u32 = 3;
44const PLP_MOD_BITS: u32 = 3;
45const PLP_ROTATION_BITS: u32 = 1;
46const PLP_FEC_TYPE_BITS: u32 = 2;
47const PLP_NUM_BLOCKS_MAX_BITS: u32 = 10;
48const FRAME_INTERVAL_BITS: u32 = 8;
49const TIME_IL_LENGTH_BITS: u32 = 8;
50const TIME_IL_TYPE_BITS: u32 = 1;
51const IN_BAND_A_FLAG_BITS: u32 = 1;
52const IN_BAND_B_FLAG_BITS: u32 = 1;
53const PLP_RESERVED_1_BITS: u32 = 11;
54const PLP_MODE_BITS: u32 = 2;
55const STATIC_FLAG_BITS: u32 = 1;
56const STATIC_PADDING_FLAG_BITS: u32 = 1;
57// post-PLP-loop
58const FEF_LENGTH_MSB_BITS: u32 = 2;
59const CONF_RESERVED_2_BITS: u32 = 30;
60// AUX loop per entry
61const AUX_STREAM_TYPE_BITS: u32 = 4;
62const AUX_PRIVATE_CONF_BITS: u32 = 28;
63
64// ── field widths: Dynamic L1-post (Figure 28) ────────────────────────────────
65
66const FRAME_IDX_BITS: u32 = 8;
67const SUB_SLICE_INTERVAL_BITS: u32 = 22;
68const TYPE_2_START_BITS: u32 = 22;
69const L1_CHANGE_COUNTER_BITS: u32 = 8;
70const START_RF_IDX_BITS: u32 = 3;
71const DYN_RESERVED_1_BITS: u32 = 8;
72// PLP dynamic loop
73const DYN_PLP_ID_BITS: u32 = 8;
74const PLP_START_BITS: u32 = 22;
75const PLP_NUM_BLOCKS_BITS: u32 = 10;
76const DYN_PLP_RESERVED_2_BITS: u32 = 8;
77// post-PLP-loop
78const DYN_RESERVED_3_BITS: u32 = 8;
79// AUX dynamic loop
80const AUX_PRIVATE_DYN_BITS: u32 = 48;
81
82// ── RF frequency entry ────────────────────────────────────────────────────────
83
84/// One entry in the RF frequency loop (Figure 27, RF loop).
85#[derive(Debug, Clone, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize))]
87#[non_exhaustive]
88pub struct RfFrequency {
89    /// RF_IDX — unique index of this frequency (3 bits, 0..NUM_RF−1).
90    pub rf_idx: u8,
91    /// FREQUENCY — centre frequency in Hz (32 bits; 0 = unknown).
92    pub frequency: u32,
93}
94
95impl RfFrequency {
96    fn parse(r: &mut BitReader<'_>) -> crate::error::Result<Self> {
97        let rf_idx = r.read_bits(RF_IDX_BITS)? as u8;
98        let frequency = r.read_bits(FREQUENCY_BITS)? as u32;
99        Ok(Self { rf_idx, frequency })
100    }
101
102    fn write(&self, w: &mut BitWriter<'_>) -> crate::error::Result<()> {
103        w.write_bits(u64::from(self.rf_idx), RF_IDX_BITS)?;
104        w.write_bits(u64::from(self.frequency), FREQUENCY_BITS)?;
105        Ok(())
106    }
107
108    const fn bits() -> usize {
109        (RF_IDX_BITS + FREQUENCY_BITS) as usize
110    }
111}
112
113// ── FEF info block ────────────────────────────────────────────────────────────
114
115/// FEF block in the configurable L1-post (Figure 27, FEF conditional).
116///
117/// Present when `S2 & 1 == 1` (S2 LSB set, i.e. mixed/FEF present).
118#[derive(Debug, Clone, PartialEq, Eq)]
119#[cfg_attr(feature = "serde", derive(serde::Serialize))]
120#[non_exhaustive]
121pub struct FefInfo {
122    /// FEF_TYPE (4 bits, Table 29 — currently all reserved).
123    pub fef_type: u8,
124    /// FEF_LENGTH — FEF length in elementary periods T (22 bits).
125    pub fef_length: u32,
126    /// FEF_INTERVAL — T2-frames between two FEF parts (8 bits).
127    pub fef_interval: u8,
128}
129
130impl FefInfo {
131    fn parse(r: &mut BitReader<'_>) -> crate::error::Result<Self> {
132        let fef_type = r.read_bits(FEF_TYPE_BITS)? as u8;
133        let fef_length = r.read_bits(FEF_LENGTH_BITS)? as u32;
134        let fef_interval = r.read_bits(FEF_INTERVAL_BITS)? as u8;
135        Ok(Self {
136            fef_type,
137            fef_length,
138            fef_interval,
139        })
140    }
141
142    fn write(&self, w: &mut BitWriter<'_>) -> crate::error::Result<()> {
143        w.write_bits(u64::from(self.fef_type), FEF_TYPE_BITS)?;
144        w.write_bits(u64::from(self.fef_length), FEF_LENGTH_BITS)?;
145        w.write_bits(u64::from(self.fef_interval), FEF_INTERVAL_BITS)?;
146        Ok(())
147    }
148
149    const fn bits() -> usize {
150        (FEF_TYPE_BITS + FEF_LENGTH_BITS + FEF_INTERVAL_BITS) as usize
151    }
152}
153
154// ── PLP configurable entry ────────────────────────────────────────────────────
155
156/// One PLP entry in the configurable L1-post loop (Figure 27, PLP loop).
157#[derive(Debug, Clone, PartialEq, Eq)]
158#[cfg_attr(feature = "serde", derive(serde::Serialize))]
159#[non_exhaustive]
160pub struct PlpConfig {
161    /// PLP_ID (8 bits).
162    pub plp_id: u8,
163    /// PLP_TYPE (3 bits, Table 30).
164    pub plp_type: PlpType,
165    /// PLP_PAYLOAD_TYPE (5 bits, Table 31).
166    pub plp_payload_type: PlpPayloadType,
167    /// FF_FLAG (1 bit).
168    pub ff_flag: bool,
169    /// FIRST_RF_IDX (3 bits).
170    pub first_rf_idx: u8,
171    /// FIRST_FRAME_IDX (8 bits).
172    pub first_frame_idx: u8,
173    /// PLP_GROUP_ID (8 bits).
174    pub plp_group_id: u8,
175    /// PLP_COD raw 3-bit value; decode with [`PlpConfig::code_rate`].
176    pub plp_cod: u8,
177    /// PLP_MOD (3 bits, Table 33).
178    pub plp_mod: PlpModulation,
179    /// PLP_ROTATION (1 bit).
180    pub plp_rotation: bool,
181    /// PLP_FEC_TYPE (2 bits, Table 34).
182    pub plp_fec_type: PlpFecType,
183    /// PLP_NUM_BLOCKS_MAX (10 bits).
184    pub plp_num_blocks_max: u16,
185    /// FRAME_INTERVAL (8 bits).
186    pub frame_interval: u8,
187    /// TIME_IL_LENGTH (8 bits).
188    pub time_il_length: u8,
189    /// TIME_IL_TYPE (1 bit).
190    pub time_il_type: bool,
191    /// IN_BAND_A_FLAG (1 bit).
192    pub in_band_a_flag: bool,
193    /// IN_BAND_B_FLAG (1 bit).
194    pub in_band_b_flag: bool,
195    /// RESERVED_1 (11 bits).
196    pub reserved_1: u16,
197    /// PLP_MODE (2 bits, Table 35).
198    pub plp_mode: PlpMode,
199    /// STATIC_FLAG (1 bit).
200    pub static_flag: bool,
201    /// STATIC_PADDING_FLAG (1 bit).
202    pub static_padding_flag: bool,
203}
204
205impl PlpConfig {
206    /// Decode the PLP code rate using the T2-base profile column of Table 32.
207    ///
208    /// The T2-Lite column differs at values 110 and 111; this returns the
209    /// T2-base interpretation. Callers working with T2-Lite profiles should
210    /// inspect `plp_cod` directly.
211    #[must_use]
212    pub fn code_rate(&self) -> PlpCodeRate {
213        PlpCodeRate::from_u8(self.plp_cod)
214    }
215
216    fn parse(r: &mut BitReader<'_>) -> crate::error::Result<Self> {
217        let plp_id = r.read_bits(PLP_ID_BITS)? as u8;
218        let plp_type = PlpType::from_u8(r.read_bits(PLP_TYPE_BITS)? as u8);
219        let plp_payload_type = PlpPayloadType::from_u8(r.read_bits(PLP_PAYLOAD_TYPE_BITS)? as u8);
220        let ff_flag = r.read_bool()?;
221        let first_rf_idx = r.read_bits(FIRST_RF_IDX_BITS)? as u8;
222        let first_frame_idx = r.read_bits(FIRST_FRAME_IDX_BITS)? as u8;
223        let plp_group_id = r.read_bits(PLP_GROUP_ID_BITS)? as u8;
224        let plp_cod = r.read_bits(PLP_COD_BITS)? as u8;
225        let plp_mod = PlpModulation::from_u8(r.read_bits(PLP_MOD_BITS)? as u8);
226        let plp_rotation = r.read_bool()?;
227        let plp_fec_type = PlpFecType::from_u8(r.read_bits(PLP_FEC_TYPE_BITS)? as u8);
228        let plp_num_blocks_max = r.read_bits(PLP_NUM_BLOCKS_MAX_BITS)? as u16;
229        let frame_interval = r.read_bits(FRAME_INTERVAL_BITS)? as u8;
230        let time_il_length = r.read_bits(TIME_IL_LENGTH_BITS)? as u8;
231        let time_il_type = r.read_bool()?;
232        let in_band_a_flag = r.read_bool()?;
233        let in_band_b_flag = r.read_bool()?;
234        let reserved_1 = r.read_bits(PLP_RESERVED_1_BITS)? as u16;
235        let plp_mode = PlpMode::from_u8(r.read_bits(PLP_MODE_BITS)? as u8);
236        let static_flag = r.read_bool()?;
237        let static_padding_flag = r.read_bool()?;
238        Ok(Self {
239            plp_id,
240            plp_type,
241            plp_payload_type,
242            ff_flag,
243            first_rf_idx,
244            first_frame_idx,
245            plp_group_id,
246            plp_cod,
247            plp_mod,
248            plp_rotation,
249            plp_fec_type,
250            plp_num_blocks_max,
251            frame_interval,
252            time_il_length,
253            time_il_type,
254            in_band_a_flag,
255            in_band_b_flag,
256            reserved_1,
257            plp_mode,
258            static_flag,
259            static_padding_flag,
260        })
261    }
262
263    fn write(&self, w: &mut BitWriter<'_>) -> crate::error::Result<()> {
264        w.write_bits(u64::from(self.plp_id), PLP_ID_BITS)?;
265        w.write_bits(u64::from(self.plp_type.to_u8()), PLP_TYPE_BITS)?;
266        w.write_bits(
267            u64::from(self.plp_payload_type.to_u8()),
268            PLP_PAYLOAD_TYPE_BITS,
269        )?;
270        w.write_bool(self.ff_flag)?;
271        w.write_bits(u64::from(self.first_rf_idx), FIRST_RF_IDX_BITS)?;
272        w.write_bits(u64::from(self.first_frame_idx), FIRST_FRAME_IDX_BITS)?;
273        w.write_bits(u64::from(self.plp_group_id), PLP_GROUP_ID_BITS)?;
274        w.write_bits(u64::from(self.plp_cod), PLP_COD_BITS)?;
275        w.write_bits(u64::from(self.plp_mod.to_u8()), PLP_MOD_BITS)?;
276        w.write_bool(self.plp_rotation)?;
277        w.write_bits(u64::from(self.plp_fec_type.to_u8()), PLP_FEC_TYPE_BITS)?;
278        w.write_bits(u64::from(self.plp_num_blocks_max), PLP_NUM_BLOCKS_MAX_BITS)?;
279        w.write_bits(u64::from(self.frame_interval), FRAME_INTERVAL_BITS)?;
280        w.write_bits(u64::from(self.time_il_length), TIME_IL_LENGTH_BITS)?;
281        w.write_bool(self.time_il_type)?;
282        w.write_bool(self.in_band_a_flag)?;
283        w.write_bool(self.in_band_b_flag)?;
284        w.write_bits(u64::from(self.reserved_1), PLP_RESERVED_1_BITS)?;
285        w.write_bits(u64::from(self.plp_mode.to_u8()), PLP_MODE_BITS)?;
286        w.write_bool(self.static_flag)?;
287        w.write_bool(self.static_padding_flag)?;
288        Ok(())
289    }
290
291    const fn bits() -> usize {
292        (PLP_ID_BITS
293            + PLP_TYPE_BITS
294            + PLP_PAYLOAD_TYPE_BITS
295            + FF_FLAG_BITS
296            + FIRST_RF_IDX_BITS
297            + FIRST_FRAME_IDX_BITS
298            + PLP_GROUP_ID_BITS
299            + PLP_COD_BITS
300            + PLP_MOD_BITS
301            + PLP_ROTATION_BITS
302            + PLP_FEC_TYPE_BITS
303            + PLP_NUM_BLOCKS_MAX_BITS
304            + FRAME_INTERVAL_BITS
305            + TIME_IL_LENGTH_BITS
306            + TIME_IL_TYPE_BITS
307            + IN_BAND_A_FLAG_BITS
308            + IN_BAND_B_FLAG_BITS
309            + PLP_RESERVED_1_BITS
310            + PLP_MODE_BITS
311            + STATIC_FLAG_BITS
312            + STATIC_PADDING_FLAG_BITS) as usize
313    }
314}
315
316// ── AUX configurable entry ────────────────────────────────────────────────────
317
318/// One entry in the configurable AUX loop (Figure 27, AUX loop).
319#[derive(Debug, Clone, PartialEq, Eq)]
320#[cfg_attr(feature = "serde", derive(serde::Serialize))]
321#[non_exhaustive]
322pub struct AuxConfig {
323    /// AUX_STREAM_TYPE (4 bits, Table 36).
324    pub aux_stream_type: AuxStreamType,
325    /// AUX_PRIVATE_CONF (28 bits).
326    pub aux_private_conf: u32,
327}
328
329impl AuxConfig {
330    fn parse(r: &mut BitReader<'_>) -> crate::error::Result<Self> {
331        let aux_stream_type = AuxStreamType::from_u8(r.read_bits(AUX_STREAM_TYPE_BITS)? as u8);
332        let aux_private_conf = r.read_bits(AUX_PRIVATE_CONF_BITS)? as u32;
333        Ok(Self {
334            aux_stream_type,
335            aux_private_conf,
336        })
337    }
338
339    fn write(&self, w: &mut BitWriter<'_>) -> crate::error::Result<()> {
340        w.write_bits(
341            u64::from(self.aux_stream_type.to_u8()),
342            AUX_STREAM_TYPE_BITS,
343        )?;
344        w.write_bits(u64::from(self.aux_private_conf), AUX_PRIVATE_CONF_BITS)?;
345        Ok(())
346    }
347
348    const fn bits() -> usize {
349        (AUX_STREAM_TYPE_BITS + AUX_PRIVATE_CONF_BITS) as usize
350    }
351}
352
353// ── L1PostConfigurable ────────────────────────────────────────────────────────
354
355/// Configurable L1-post signalling — EN 302 755 §7.2.3.1, Figure 27.
356///
357/// Parsed from the `L1CONF` section of a T2-MI L1-current payload.
358/// The caller provides `num_rf` (from L1-pre) and `fef_present` (S2 LSB).
359#[derive(Debug, Clone, PartialEq, Eq)]
360#[cfg_attr(feature = "serde", derive(serde::Serialize))]
361#[non_exhaustive]
362pub struct L1PostConfigurable {
363    /// SUB_SLICES_PER_FRAME (15 bits).
364    pub sub_slices_per_frame: u16,
365    /// NUM_PLP (8 bits) — read from the stream; also determines `plps.len()`.
366    pub num_plp: u8,
367    /// NUM_AUX (4 bits) — read from the stream; also determines `aux.len()`.
368    pub num_aux: u8,
369    /// AUX_CONFIG_RFU (8 bits).
370    pub aux_config_rfu: u8,
371    /// RF frequency loop (length = `num_rf` argument).
372    pub rf: Vec<RfFrequency>,
373    /// FEF block — present when `fef_present` was `true`.
374    pub fef: Option<FefInfo>,
375    /// PLP loop (length = `num_plp`).
376    pub plps: Vec<PlpConfig>,
377    /// FEF_LENGTH_MSB (2 bits, post-PLP-loop).
378    pub fef_length_msb: u8,
379    /// RESERVED_2 (30 bits, post-PLP-loop).
380    pub reserved_2: u32,
381    /// AUX loop (length = `num_aux`).
382    pub aux: Vec<AuxConfig>,
383}
384
385impl L1PostConfigurable {
386    /// Parse from `bytes`, using `num_rf` from L1-pre and `fef_present` from S2 LSB.
387    ///
388    /// # Errors
389    /// [`crate::Error::L1Bits`] on bit-stream overrun.
390    pub fn parse(bytes: &[u8], num_rf: u8, fef_present: bool) -> crate::error::Result<Self> {
391        let mut r = BitReader::new(bytes);
392
393        let sub_slices_per_frame = r.read_bits(SUB_SLICES_PER_FRAME_BITS)? as u16;
394        let num_plp = r.read_bits(NUM_PLP_BITS)? as u8;
395        let num_aux = r.read_bits(NUM_AUX_BITS)? as u8;
396        let aux_config_rfu = r.read_bits(AUX_CONFIG_RFU_BITS)? as u8;
397
398        let mut rf = Vec::with_capacity(num_rf as usize);
399        for _ in 0..num_rf {
400            rf.push(RfFrequency::parse(&mut r)?);
401        }
402
403        let fef = if fef_present {
404            Some(FefInfo::parse(&mut r)?)
405        } else {
406            None
407        };
408
409        let mut plps = Vec::with_capacity(num_plp as usize);
410        for _ in 0..num_plp {
411            plps.push(PlpConfig::parse(&mut r)?);
412        }
413
414        let fef_length_msb = r.read_bits(FEF_LENGTH_MSB_BITS)? as u8;
415        let reserved_2 = r.read_bits(CONF_RESERVED_2_BITS)? as u32;
416
417        let mut aux = Vec::with_capacity(num_aux as usize);
418        for _ in 0..num_aux {
419            aux.push(AuxConfig::parse(&mut r)?);
420        }
421
422        Ok(Self {
423            sub_slices_per_frame,
424            num_plp,
425            num_aux,
426            aux_config_rfu,
427            rf,
428            fef,
429            plps,
430            fef_length_msb,
431            reserved_2,
432            aux,
433        })
434    }
435
436    /// Compute the total bit-length of this configurable block (without byte padding).
437    #[must_use]
438    pub fn len_bits(&self) -> usize {
439        let header = (SUB_SLICES_PER_FRAME_BITS + NUM_PLP_BITS + NUM_AUX_BITS + AUX_CONFIG_RFU_BITS)
440            as usize;
441        let rf_bits = self.rf.len() * RfFrequency::bits();
442        let fef_bits = if self.fef.is_some() {
443            FefInfo::bits()
444        } else {
445            0
446        };
447        let plp_bits = self.plps.len() * PlpConfig::bits();
448        let post_loop = (FEF_LENGTH_MSB_BITS + CONF_RESERVED_2_BITS) as usize;
449        let aux_bits = self.aux.len() * AuxConfig::bits();
450        header + rf_bits + fef_bits + plp_bits + post_loop + aux_bits
451    }
452
453    /// Serialise into a pre-allocated bit-writer.
454    ///
455    /// # Errors
456    /// [`crate::Error::L1Bits`] if the writer's buffer is too small.
457    pub fn serialize_bits(&self, w: &mut BitWriter<'_>) -> crate::error::Result<()> {
458        w.write_bits(
459            u64::from(self.sub_slices_per_frame),
460            SUB_SLICES_PER_FRAME_BITS,
461        )?;
462        w.write_bits(u64::from(self.num_plp), NUM_PLP_BITS)?;
463        w.write_bits(u64::from(self.num_aux), NUM_AUX_BITS)?;
464        w.write_bits(u64::from(self.aux_config_rfu), AUX_CONFIG_RFU_BITS)?;
465        for rf in &self.rf {
466            rf.write(w)?;
467        }
468        if let Some(fef) = &self.fef {
469            fef.write(w)?;
470        }
471        for plp in &self.plps {
472            plp.write(w)?;
473        }
474        w.write_bits(u64::from(self.fef_length_msb), FEF_LENGTH_MSB_BITS)?;
475        w.write_bits(u64::from(self.reserved_2), CONF_RESERVED_2_BITS)?;
476        for aux in &self.aux {
477            aux.write(w)?;
478        }
479        Ok(())
480    }
481}
482
483// ── PLP dynamic entry ─────────────────────────────────────────────────────────
484
485/// One PLP entry in the dynamic L1-post loop (Figure 28, PLP loop).
486#[derive(Debug, Clone, PartialEq, Eq)]
487#[cfg_attr(feature = "serde", derive(serde::Serialize))]
488#[non_exhaustive]
489pub struct PlpDynamic {
490    /// PLP_ID (8 bits).
491    pub plp_id: u8,
492    /// PLP_START — start position (cell addressing, 22 bits).
493    pub plp_start: u32,
494    /// PLP_NUM_BLOCKS (10 bits).
495    pub plp_num_blocks: u16,
496    /// RESERVED_2 (8 bits).
497    pub reserved_2: u8,
498}
499
500impl PlpDynamic {
501    fn parse(r: &mut BitReader<'_>) -> crate::error::Result<Self> {
502        let plp_id = r.read_bits(DYN_PLP_ID_BITS)? as u8;
503        let plp_start = r.read_bits(PLP_START_BITS)? as u32;
504        let plp_num_blocks = r.read_bits(PLP_NUM_BLOCKS_BITS)? as u16;
505        let reserved_2 = r.read_bits(DYN_PLP_RESERVED_2_BITS)? as u8;
506        Ok(Self {
507            plp_id,
508            plp_start,
509            plp_num_blocks,
510            reserved_2,
511        })
512    }
513
514    fn write(&self, w: &mut BitWriter<'_>) -> crate::error::Result<()> {
515        w.write_bits(u64::from(self.plp_id), DYN_PLP_ID_BITS)?;
516        w.write_bits(u64::from(self.plp_start), PLP_START_BITS)?;
517        w.write_bits(u64::from(self.plp_num_blocks), PLP_NUM_BLOCKS_BITS)?;
518        w.write_bits(u64::from(self.reserved_2), DYN_PLP_RESERVED_2_BITS)?;
519        Ok(())
520    }
521
522    const fn bits() -> usize {
523        (DYN_PLP_ID_BITS + PLP_START_BITS + PLP_NUM_BLOCKS_BITS + DYN_PLP_RESERVED_2_BITS) as usize
524    }
525}
526
527// ── AUX dynamic entry ─────────────────────────────────────────────────────────
528
529/// One entry in the dynamic AUX loop (Figure 28, AUX loop).
530#[derive(Debug, Clone, PartialEq, Eq)]
531#[cfg_attr(feature = "serde", derive(serde::Serialize))]
532#[non_exhaustive]
533pub struct AuxDynamic {
534    /// AUX_PRIVATE_DYN (48 bits).
535    pub aux_private_dyn: u64,
536}
537
538impl AuxDynamic {
539    fn parse(r: &mut BitReader<'_>) -> crate::error::Result<Self> {
540        let aux_private_dyn = r.read_bits(AUX_PRIVATE_DYN_BITS)?;
541        Ok(Self { aux_private_dyn })
542    }
543
544    fn write(&self, w: &mut BitWriter<'_>) -> crate::error::Result<()> {
545        w.write_bits(self.aux_private_dyn, AUX_PRIVATE_DYN_BITS)?;
546        Ok(())
547    }
548
549    const fn bits() -> usize {
550        AUX_PRIVATE_DYN_BITS as usize
551    }
552}
553
554// ── L1PostDynamic ─────────────────────────────────────────────────────────────
555
556/// Dynamic L1-post signalling — EN 302 755 §7.2.3.2, Figure 28.
557///
558/// Parse with `L1PostDynamic::parse(bytes, num_plp, num_aux)`.
559/// `num_plp` and `num_aux` come from the configurable block's `num_plp` /
560/// `num_aux` fields.
561#[derive(Debug, Clone, PartialEq, Eq)]
562#[cfg_attr(feature = "serde", derive(serde::Serialize))]
563#[non_exhaustive]
564pub struct L1PostDynamic {
565    /// FRAME_IDX (8 bits).
566    pub frame_idx: u8,
567    /// SUB_SLICE_INTERVAL (22 bits; 0 if no type-2 PLPs).
568    pub sub_slice_interval: u32,
569    /// TYPE_2_START (22 bits; 0 if no type-2 PLPs).
570    pub type_2_start: u32,
571    /// L1_CHANGE_COUNTER (8 bits; 0 = no scheduled change).
572    pub l1_change_counter: u8,
573    /// START_RF_IDX (3 bits; 0 if TFS not used).
574    pub start_rf_idx: u8,
575    /// RESERVED_1 (8 bits).
576    pub reserved_1: u8,
577    /// PLP dynamic loop (length = `num_plp`).
578    pub plps: Vec<PlpDynamic>,
579    /// RESERVED_3 (8 bits, post-PLP-loop).
580    pub reserved_3: u8,
581    /// AUX dynamic loop (length = `num_aux`).
582    pub aux: Vec<AuxDynamic>,
583}
584
585impl L1PostDynamic {
586    /// Parse from `bytes` with `num_plp` and `num_aux` context from the
587    /// configurable block.
588    ///
589    /// # Errors
590    /// [`crate::Error::L1Bits`] on bit-stream overrun.
591    pub fn parse(bytes: &[u8], num_plp: u8, num_aux: u8) -> crate::error::Result<Self> {
592        let mut r = BitReader::new(bytes);
593
594        let frame_idx = r.read_bits(FRAME_IDX_BITS)? as u8;
595        let sub_slice_interval = r.read_bits(SUB_SLICE_INTERVAL_BITS)? as u32;
596        let type_2_start = r.read_bits(TYPE_2_START_BITS)? as u32;
597        let l1_change_counter = r.read_bits(L1_CHANGE_COUNTER_BITS)? as u8;
598        let start_rf_idx = r.read_bits(START_RF_IDX_BITS)? as u8;
599        let reserved_1 = r.read_bits(DYN_RESERVED_1_BITS)? as u8;
600
601        let mut plps = Vec::with_capacity(num_plp as usize);
602        for _ in 0..num_plp {
603            plps.push(PlpDynamic::parse(&mut r)?);
604        }
605
606        let reserved_3 = r.read_bits(DYN_RESERVED_3_BITS)? as u8;
607
608        let mut aux = Vec::with_capacity(num_aux as usize);
609        for _ in 0..num_aux {
610            aux.push(AuxDynamic::parse(&mut r)?);
611        }
612
613        Ok(Self {
614            frame_idx,
615            sub_slice_interval,
616            type_2_start,
617            l1_change_counter,
618            start_rf_idx,
619            reserved_1,
620            plps,
621            reserved_3,
622            aux,
623        })
624    }
625
626    /// Compute total bit-length (without byte padding).
627    #[must_use]
628    pub fn len_bits(&self) -> usize {
629        let header = (FRAME_IDX_BITS
630            + SUB_SLICE_INTERVAL_BITS
631            + TYPE_2_START_BITS
632            + L1_CHANGE_COUNTER_BITS
633            + START_RF_IDX_BITS
634            + DYN_RESERVED_1_BITS) as usize;
635        let plp_bits = self.plps.len() * PlpDynamic::bits();
636        let post = DYN_RESERVED_3_BITS as usize;
637        let aux_bits = self.aux.len() * AuxDynamic::bits();
638        header + plp_bits + post + aux_bits
639    }
640
641    /// Serialise into a pre-allocated bit-writer.
642    ///
643    /// # Errors
644    /// [`crate::Error::L1Bits`] if the writer's buffer is too small.
645    pub fn serialize_bits(&self, w: &mut BitWriter<'_>) -> crate::error::Result<()> {
646        w.write_bits(u64::from(self.frame_idx), FRAME_IDX_BITS)?;
647        w.write_bits(u64::from(self.sub_slice_interval), SUB_SLICE_INTERVAL_BITS)?;
648        w.write_bits(u64::from(self.type_2_start), TYPE_2_START_BITS)?;
649        w.write_bits(u64::from(self.l1_change_counter), L1_CHANGE_COUNTER_BITS)?;
650        w.write_bits(u64::from(self.start_rf_idx), START_RF_IDX_BITS)?;
651        w.write_bits(u64::from(self.reserved_1), DYN_RESERVED_1_BITS)?;
652        for plp in &self.plps {
653            plp.write(w)?;
654        }
655        w.write_bits(u64::from(self.reserved_3), DYN_RESERVED_3_BITS)?;
656        for aux in &self.aux {
657            aux.write(w)?;
658        }
659        Ok(())
660    }
661}
662
663// ── L1ExtBlock ────────────────────────────────────────────────────────────────
664
665const L1_EXT_BLOCK_TYPE_BITS: u32 = 8;
666const L1_EXT_DATA_LEN_BITS: u32 = 16;
667
668/// One extension block — EN 302 755 §7.2.3.4, Table 37.
669///
670/// Within the L1EXT region the blocks are bit-contiguous (a block's data is
671/// `data_bit_len` bits, not byte-padded); only the whole region is zero-padded
672/// to a byte boundary. `data` holds `ceil(data_bit_len/8)` bytes, MSB-first,
673/// with the final byte zero-padded in its low bits.
674#[derive(Debug, Clone, PartialEq, Eq)]
675#[cfg_attr(feature = "serde", derive(serde::Serialize))]
676#[non_exhaustive]
677pub struct L1ExtBlock {
678    /// L1_EXT_BLOCK_TYPE (8 bits, Table 38).
679    pub block_type: u8,
680    /// L1_EXT_DATA_LEN — length of `data` in bits (16-bit wire field).
681    pub data_bit_len: u16,
682    /// L1_EXT_BLOCK_DATA — `ceil(data_bit_len/8)` bytes, MSB-first.
683    pub data: Vec<u8>,
684}
685
686impl L1ExtBlock {
687    /// Bits this block occupies in the L1EXT region (header + data, no padding).
688    fn bits(&self) -> usize {
689        (L1_EXT_BLOCK_TYPE_BITS + L1_EXT_DATA_LEN_BITS) as usize + self.data_bit_len as usize
690    }
691
692    fn parse(r: &mut BitReader<'_>) -> crate::error::Result<Self> {
693        let block_type = r.read_bits(L1_EXT_BLOCK_TYPE_BITS)? as u8;
694        let data_bit_len = r.read_bits(L1_EXT_DATA_LEN_BITS)? as u16;
695        let n = data_bit_len as usize;
696        let mut data = vec![0u8; n.div_ceil(8)];
697        let mut done = 0usize;
698        for byte in data.iter_mut() {
699            let take = (n - done).min(8) as u32;
700            let v = r.read_bits(take)? as u8;
701            // MSB-first: a short final field sits in the high bits.
702            *byte = if take < 8 { v << (8 - take) } else { v };
703            done += take as usize;
704        }
705        Ok(Self {
706            block_type,
707            data_bit_len,
708            data,
709        })
710    }
711
712    fn write(&self, w: &mut BitWriter<'_>) -> crate::error::Result<()> {
713        w.write_bits(u64::from(self.block_type), L1_EXT_BLOCK_TYPE_BITS)?;
714        w.write_bits(u64::from(self.data_bit_len), L1_EXT_DATA_LEN_BITS)?;
715        let n = self.data_bit_len as usize;
716        let mut done = 0usize;
717        for &byte in &self.data {
718            let take = (n - done).min(8) as u32;
719            let v = if take < 8 {
720                u64::from(byte >> (8 - take))
721            } else {
722                u64::from(byte)
723            };
724            w.write_bits(v, take)?;
725            done += take as usize;
726        }
727        Ok(())
728    }
729}
730
731/// Parse the extension blocks filling the first `total_bits` of `bytes`.
732///
733/// Blocks are bit-contiguous; the loop stops when fewer than a block header
734/// (24 bits) of declared content remains (the trailing zero-pad is < 8 bits).
735fn parse_ext_blocks(bytes: &[u8], total_bits: usize) -> crate::error::Result<Vec<L1ExtBlock>> {
736    let mut r = BitReader::new(bytes);
737    let header_bits = (L1_EXT_BLOCK_TYPE_BITS + L1_EXT_DATA_LEN_BITS) as usize;
738    let mut blocks = Vec::new();
739    while r.bits_read() + header_bits <= total_bits {
740        blocks.push(L1ExtBlock::parse(&mut r)?);
741    }
742    Ok(blocks)
743}
744
745// ── L1Post ────────────────────────────────────────────────────────────────────
746
747/// Parsed L1-post signalling block.
748///
749/// Produced by [`crate::payload::l1_current::L1CurrentPayload::l1_post`].
750#[derive(Debug, Clone, PartialEq, Eq)]
751#[cfg_attr(feature = "serde", derive(serde::Serialize))]
752#[non_exhaustive]
753pub struct L1Post {
754    /// Configurable part (Figure 27).
755    pub configurable: L1PostConfigurable,
756    /// Dynamic part for the current frame (Figure 28).
757    pub dynamic_current: L1PostDynamic,
758    /// Dynamic part for the next frame — present when L1-pre `L1_REPETITION_FLAG = 1`
759    /// (§7.2.3.3). `None` for L1-current payloads (the framing carries only the
760    /// current-frame dynamic block).
761    pub dynamic_next: Option<L1PostDynamic>,
762    /// Extension blocks (Table 37). Empty when `L1_POST_EXTENSION = 0`.
763    pub extension: Vec<L1ExtBlock>,
764}
765
766impl L1Post {
767    /// Serialise back to the framed T2-MI L1-current data region — the bytes
768    /// that follow the 21-byte L1PRE inside `L1CurrentPayload::l1_current_data`
769    /// (TS 102 773 Table 2). Symmetric with the parse performed by
770    /// [`crate::payload::L1CurrentPayload::l1_post`].
771    ///
772    /// # Errors
773    /// [`crate::Error::L1Bits`] on an internal bit-buffer overrun (cannot occur
774    /// for a value obtained by parsing valid wire data).
775    pub fn to_l1_current_framed(&self) -> crate::error::Result<Vec<u8>> {
776        serialize_l1_post_framed(self)
777    }
778}
779
780/// Parse the framed T2-MI L1-current data region (everything after the 21-byte L1PRE).
781///
782/// Layout per TS 102 773 Table 2:
783/// ```text
784/// L1CONF_LEN (16 bits)
785/// L1CONF     (ceil(L1CONF_LEN/8) bytes, zero-padded)
786/// L1DYN_CURR_LEN (16 bits)
787/// L1DYN_CURR     (ceil(L1DYN_CURR_LEN/8) bytes, zero-padded)
788/// L1EXT_LEN  (16 bits)
789/// L1EXT      (ceil(L1EXT_LEN/8) bytes, zero-padded)
790/// ```
791///
792/// `num_rf` and `fef_present` come from the L1-pre block.
793pub(crate) fn parse_l1_post_from_framed(
794    framed: &[u8],
795    num_rf: u8,
796    fef_present: bool,
797) -> crate::error::Result<L1Post> {
798    let mut pos = 0usize;
799
800    // ── L1CONF ────────────────────────────────────────────────────────────────
801    let (b, _) = framed
802        .get(pos..)
803        .and_then(|s| s.split_first_chunk::<2>())
804        .ok_or(crate::Error::BufferTooShort {
805            need: pos + 2,
806            have: framed.len(),
807            what: "L1CONF_LEN",
808        })?;
809    let l1conf_len_bits = u16::from_be_bytes(*b) as usize;
810    pos += 2;
811    let l1conf_bytes = l1conf_len_bits.div_ceil(8);
812    if framed.len() < pos + l1conf_bytes {
813        return Err(crate::Error::BufferTooShort {
814            need: pos + l1conf_bytes,
815            have: framed.len(),
816            what: "L1CONF",
817        });
818    }
819    let configurable =
820        L1PostConfigurable::parse(&framed[pos..pos + l1conf_bytes], num_rf, fef_present)?;
821    pos += l1conf_bytes;
822
823    let num_plp = configurable.num_plp;
824    let num_aux = configurable.num_aux;
825
826    // ── L1DYN_CURR ────────────────────────────────────────────────────────────
827    let (b, _) = framed
828        .get(pos..)
829        .and_then(|s| s.split_first_chunk::<2>())
830        .ok_or(crate::Error::BufferTooShort {
831            need: pos + 2,
832            have: framed.len(),
833            what: "L1DYN_CURR_LEN",
834        })?;
835    let l1dyn_len_bits = u16::from_be_bytes(*b) as usize;
836    pos += 2;
837    let l1dyn_bytes = l1dyn_len_bits.div_ceil(8);
838    if framed.len() < pos + l1dyn_bytes {
839        return Err(crate::Error::BufferTooShort {
840            need: pos + l1dyn_bytes,
841            have: framed.len(),
842            what: "L1DYN_CURR",
843        });
844    }
845    let dynamic_current = L1PostDynamic::parse(&framed[pos..pos + l1dyn_bytes], num_plp, num_aux)?;
846    pos += l1dyn_bytes;
847
848    // ── L1EXT ─────────────────────────────────────────────────────────────────
849    let (b, _) = framed
850        .get(pos..)
851        .and_then(|s| s.split_first_chunk::<2>())
852        .ok_or(crate::Error::BufferTooShort {
853            need: pos + 2,
854            have: framed.len(),
855            what: "L1EXT_LEN",
856        })?;
857    let l1ext_len_bits = u16::from_be_bytes(*b) as usize;
858    pos += 2;
859    let l1ext_bytes = l1ext_len_bits.div_ceil(8);
860    if framed.len() < pos + l1ext_bytes {
861        return Err(crate::Error::BufferTooShort {
862            need: pos + l1ext_bytes,
863            have: framed.len(),
864            what: "L1EXT",
865        });
866    }
867    let extension = if l1ext_bytes > 0 {
868        parse_ext_blocks(&framed[pos..pos + l1ext_bytes], l1ext_len_bits)?
869    } else {
870        Vec::new()
871    };
872
873    Ok(L1Post {
874        configurable,
875        dynamic_current,
876        dynamic_next: None, // L1-current carries only the current-frame dynamic
877        extension,
878    })
879}
880
881/// Append one framed block: a 16-bit bit-length, then the bit-packed content
882/// zero-padded up to a byte boundary.
883fn push_framed_block<F>(out: &mut Vec<u8>, bit_len: usize, write: F) -> crate::error::Result<()>
884where
885    F: FnOnce(&mut BitWriter<'_>) -> crate::error::Result<()>,
886{
887    out.extend_from_slice(&(bit_len as u16).to_be_bytes());
888    let start = out.len();
889    out.resize(start + bit_len.div_ceil(8), 0);
890    let mut w = BitWriter::new(&mut out[start..]);
891    write(&mut w)?;
892    Ok(())
893}
894
895/// Serialise an [`L1Post`] back to the framed T2-MI L1-current data region
896/// (everything after the 21-byte L1PRE): `L1CONF_LEN`+`L1CONF`,
897/// `L1DYN_CURR_LEN`+`L1DYN_CURR`, `L1EXT_LEN`+`L1EXT` (TS 102 773 Table 2).
898/// Each block is zero-padded to a byte boundary. `dynamic_next` is not part of
899/// the L1-current framing (it is carried in L1-future) and is ignored here.
900pub(crate) fn serialize_l1_post_framed(post: &L1Post) -> crate::error::Result<Vec<u8>> {
901    let mut out = Vec::new();
902    push_framed_block(&mut out, post.configurable.len_bits(), |w| {
903        post.configurable.serialize_bits(w)
904    })?;
905    push_framed_block(&mut out, post.dynamic_current.len_bits(), |w| {
906        post.dynamic_current.serialize_bits(w)
907    })?;
908    let ext_bits: usize = post.extension.iter().map(L1ExtBlock::bits).sum();
909    push_framed_block(&mut out, ext_bits, |w| {
910        for block in &post.extension {
911            block.write(w)?;
912        }
913        Ok(())
914    })?;
915    Ok(out)
916}
917
918#[cfg(test)]
919mod tests {
920    use super::*;
921
922    fn synthetic_conf(
923        num_rf: u8,
924        fef_present: bool,
925        num_plp: u8,
926        num_aux: u8,
927    ) -> L1PostConfigurable {
928        let rf = (0..num_rf)
929            .map(|i| RfFrequency {
930                rf_idx: i,
931                frequency: 666_000_000 + u32::from(i) * 8_000_000,
932            })
933            .collect();
934        let fef = if fef_present {
935            Some(FefInfo {
936                fef_type: 0,
937                fef_length: 100,
938                fef_interval: 4,
939            })
940        } else {
941            None
942        };
943        let plps = (0..num_plp)
944            .map(|i| PlpConfig {
945                plp_id: i,
946                plp_type: PlpType::Common,
947                plp_payload_type: PlpPayloadType::Ts,
948                ff_flag: false,
949                first_rf_idx: 0,
950                first_frame_idx: 0,
951                plp_group_id: 0,
952                plp_cod: 0,
953                plp_mod: PlpModulation::Qam16,
954                plp_rotation: false,
955                plp_fec_type: PlpFecType::Ldpc16K,
956                plp_num_blocks_max: 10,
957                frame_interval: 1,
958                time_il_length: 0,
959                time_il_type: false,
960                in_band_a_flag: false,
961                in_band_b_flag: false,
962                reserved_1: 0,
963                plp_mode: PlpMode::Normal,
964                static_flag: false,
965                static_padding_flag: false,
966            })
967            .collect();
968        let aux = (0..num_aux)
969            .map(|_| AuxConfig {
970                aux_stream_type: AuxStreamType::TxSig,
971                aux_private_conf: 0,
972            })
973            .collect();
974        L1PostConfigurable {
975            sub_slices_per_frame: 512,
976            num_plp,
977            num_aux,
978            aux_config_rfu: 0,
979            rf,
980            fef,
981            plps,
982            fef_length_msb: 0,
983            reserved_2: 0,
984            aux,
985        }
986    }
987
988    fn synthetic_dyn(num_plp: u8, num_aux: u8) -> L1PostDynamic {
989        let plps = (0..num_plp)
990            .map(|i| PlpDynamic {
991                plp_id: i,
992                plp_start: 0x1234,
993                plp_num_blocks: 5,
994                reserved_2: 0,
995            })
996            .collect();
997        let aux = (0..num_aux)
998            .map(|_| AuxDynamic { aux_private_dyn: 0 })
999            .collect();
1000        L1PostDynamic {
1001            frame_idx: 0,
1002            sub_slice_interval: 0,
1003            type_2_start: 0,
1004            l1_change_counter: 0,
1005            start_rf_idx: 0,
1006            reserved_1: 0,
1007            plps,
1008            reserved_3: 0,
1009            aux,
1010        }
1011    }
1012
1013    #[test]
1014    fn configurable_round_trip() {
1015        let conf = synthetic_conf(1, false, 1, 0);
1016        let bits = conf.len_bits();
1017        let bytes = bits.div_ceil(8);
1018        let mut buf = vec![0u8; bytes];
1019        let mut w = BitWriter::new(&mut buf);
1020        conf.serialize_bits(&mut w).unwrap();
1021        let parsed = L1PostConfigurable::parse(&buf, 1, false).unwrap();
1022        assert_eq!(conf, parsed);
1023    }
1024
1025    #[test]
1026    fn configurable_round_trip_with_fef() {
1027        let conf = synthetic_conf(1, true, 1, 0);
1028        let bits = conf.len_bits();
1029        let bytes = bits.div_ceil(8);
1030        let mut buf = vec![0u8; bytes];
1031        let mut w = BitWriter::new(&mut buf);
1032        conf.serialize_bits(&mut w).unwrap();
1033        let parsed = L1PostConfigurable::parse(&buf, 1, true).unwrap();
1034        assert_eq!(conf, parsed);
1035    }
1036
1037    #[test]
1038    fn dynamic_round_trip() {
1039        let dyn_ = synthetic_dyn(1, 0);
1040        let bits = dyn_.len_bits();
1041        let bytes = bits.div_ceil(8);
1042        let mut buf = vec![0u8; bytes];
1043        let mut w = BitWriter::new(&mut buf);
1044        dyn_.serialize_bits(&mut w).unwrap();
1045        let parsed = L1PostDynamic::parse(&buf, 1, 0).unwrap();
1046        assert_eq!(dyn_, parsed);
1047    }
1048
1049    #[test]
1050    fn plp_config_bits_constant() {
1051        // Figure 27 PLP entry: 8+3+5+1+3+8+8+3+3+1+2+10+8+8+1+1+1+11+2+1+1 = 89
1052        assert_eq!(PlpConfig::bits(), 89);
1053    }
1054
1055    #[test]
1056    fn plp_dynamic_bits_constant() {
1057        // Figure 28 PLP entry: 8+22+10+8 = 48
1058        assert_eq!(PlpDynamic::bits(), 48);
1059    }
1060
1061    #[test]
1062    fn plp_code_rate_accessor() {
1063        let plp = PlpConfig {
1064            plp_id: 0,
1065            plp_type: PlpType::Common,
1066            plp_payload_type: PlpPayloadType::Ts,
1067            ff_flag: false,
1068            first_rf_idx: 0,
1069            first_frame_idx: 0,
1070            plp_group_id: 0,
1071            plp_cod: 2, // R2_3
1072            plp_mod: PlpModulation::Qam64,
1073            plp_rotation: false,
1074            plp_fec_type: PlpFecType::Ldpc64K,
1075            plp_num_blocks_max: 20,
1076            frame_interval: 1,
1077            time_il_length: 0,
1078            time_il_type: false,
1079            in_band_a_flag: false,
1080            in_band_b_flag: false,
1081            reserved_1: 0,
1082            plp_mode: PlpMode::Normal,
1083            static_flag: false,
1084            static_padding_flag: false,
1085        };
1086        assert_eq!(plp.code_rate(), PlpCodeRate::R2_3);
1087    }
1088
1089    #[test]
1090    fn configurable_header_bits() {
1091        // Header: 15+8+4+8 = 35, RF×1: 3+32=35, FEF=0, PLP×1=89, post=2+30=32, AUX×0=0
1092        // Total = 35+35+89+32 = 191
1093        let conf = synthetic_conf(1, false, 1, 0);
1094        assert_eq!(conf.len_bits(), 191);
1095    }
1096
1097    #[test]
1098    fn dynamic_header_bits() {
1099        // Header: 8+22+22+8+3+8 = 71, PLP×1=48, post=8, AUX×0=0
1100        // Total = 71+48+8 = 127
1101        let dyn_ = synthetic_dyn(1, 0);
1102        assert_eq!(dyn_.len_bits(), 127);
1103    }
1104
1105    #[test]
1106    fn l1post_framed_round_trip() {
1107        // Full L1-current framing round-trip (no extension blocks).
1108        let post = L1Post {
1109            configurable: synthetic_conf(1, false, 1, 0),
1110            dynamic_current: synthetic_dyn(1, 0),
1111            dynamic_next: None,
1112            extension: Vec::new(),
1113        };
1114        let framed = post.to_l1_current_framed().unwrap();
1115        // 2 + ceil(191/8)=24 + 2 + ceil(127/8)=16 + 2 + 0 = 46 bytes
1116        assert_eq!(framed.len(), 46);
1117        let parsed = parse_l1_post_from_framed(&framed, 1, false).unwrap();
1118        assert_eq!(post, parsed);
1119    }
1120
1121    #[test]
1122    fn l1post_framed_round_trip_with_nonaligned_ext() {
1123        // A 12-bit (non-byte-aligned) extension block exercises the
1124        // bit-contiguous L1ExtBlock parse/serialize path.
1125        let post = L1Post {
1126            configurable: synthetic_conf(2, true, 1, 1),
1127            dynamic_current: synthetic_dyn(1, 1),
1128            dynamic_next: None,
1129            extension: vec![L1ExtBlock {
1130                block_type: 0xFF,
1131                data_bit_len: 12,
1132                data: vec![0xAB, 0xC0], // 12 bits: 0xAB then high nibble 0xC, low 4 bits zero-pad
1133            }],
1134        };
1135        let framed = post.to_l1_current_framed().unwrap();
1136        let parsed = parse_l1_post_from_framed(&framed, 2, true).unwrap();
1137        assert_eq!(post, parsed);
1138    }
1139}