Skip to main content

dvb_si/descriptors/
terrestrial_delivery_system.rs

1//! Terrestrial Delivery System Descriptor — ETSI EN 300 468 §6.2.13.4 (tag 0x5A).
2//!
3//! Carried inside the NIT's transport\_stream\_loop second descriptor loop for
4//! DVB-T transponders. Expresses the full DVB-T PHY configuration needed to
5//! tune the carrier.
6
7use super::descriptor_body;
8use crate::error::{Error, Result};
9use dvb_common::{Parse, Serialize};
10
11/// Descriptor tag for terrestrial\_delivery\_system\_descriptor.
12pub const TAG: u8 = 0x5A;
13const HEADER_LEN: usize = 2;
14const BODY_LEN: u8 = 11;
15
16const BW_SHIFT: u8 = 5;
17const PRIORITY_MASK: u8 = 0b0001_0000;
18const TIME_SLICING_MASK: u8 = 0b0000_1000;
19const MPE_FEC_MASK: u8 = 0b0000_0100;
20const RESERVED_FU_MASK: u8 = 0b0000_0011;
21const BW_MASK: u8 = 0b1110_0000;
22
23const CONSTELLATION_SHIFT: u8 = 6;
24const HIERARCHY_SHIFT: u8 = 3;
25const CONSTELLATION_MASK: u8 = 0b1100_0000;
26const HIERARCHY_MASK: u8 = 0b0011_1000;
27const CODE_RATE_HP_MASK: u8 = 0b0000_0111;
28
29const CODE_RATE_LP_SHIFT: u8 = 5;
30const GUARD_INTERVAL_SHIFT: u8 = 3;
31const TRANSMISSION_MODE_SHIFT: u8 = 1;
32const CODE_RATE_LP_MASK: u8 = 0b1110_0000;
33const GUARD_INTERVAL_MASK: u8 = 0b0001_1000;
34const TRANSMISSION_MODE_MASK: u8 = 0b0000_0110;
35const OTHER_FREQ_FLAG_MASK: u8 = 0b0000_0001;
36
37const TRAILING_RESERVED: u32 = 0xFFFF_FFFF;
38
39/// Channel bandwidth (§6.2.13.4 Table 52).
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize))]
42#[non_exhaustive]
43pub enum Bandwidth {
44    /// 8 MHz.
45    Mhz8,
46    /// 7 MHz.
47    Mhz7,
48    /// 6 MHz.
49    Mhz6,
50    /// 5 MHz.
51    Mhz5,
52    /// Unspecified / reserved value.
53    Reserved(u8),
54}
55
56impl Bandwidth {
57    /// Human-readable spec label (ETSI EN 300 468 §6.2.13.4 Table 52).
58    #[must_use]
59    pub fn name(self) -> &'static str {
60        match self {
61            Self::Mhz8 => "8 MHz",
62            Self::Mhz7 => "7 MHz",
63            Self::Mhz6 => "6 MHz",
64            Self::Mhz5 => "5 MHz",
65            Self::Reserved(_) => "reserved",
66        }
67    }
68}
69dvb_common::impl_spec_display!(Bandwidth, Reserved);
70
71/// Constellation.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73#[cfg_attr(feature = "serde", derive(serde::Serialize))]
74#[non_exhaustive]
75pub enum Constellation {
76    /// QPSK.
77    Qpsk,
78    /// 16-QAM.
79    Qam16,
80    /// 64-QAM.
81    Qam64,
82    /// Unspecified / reserved value.
83    Reserved(u8),
84}
85
86impl Constellation {
87    /// Human-readable spec label (ETSI EN 300 468 §6.2.13.4 Table 52).
88    #[must_use]
89    pub fn name(self) -> &'static str {
90        match self {
91            Self::Qpsk => "QPSK",
92            Self::Qam16 => "16-QAM",
93            Self::Qam64 => "64-QAM",
94            Self::Reserved(_) => "reserved",
95        }
96    }
97}
98dvb_common::impl_spec_display!(Constellation, Reserved);
99
100/// Hierarchy mode — combines native/in-depth interleaver and α.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize))]
103#[non_exhaustive]
104pub enum Hierarchy {
105    /// Non-hierarchical + native.
106    NonHierarchicalNative,
107    /// α=1 + native.
108    Alpha1Native,
109    /// α=2 + native.
110    Alpha2Native,
111    /// α=4 + native.
112    Alpha4Native,
113    /// Non-hierarchical + in-depth.
114    NonHierarchicalInDepth,
115    /// α=1 + in-depth.
116    Alpha1InDepth,
117    /// α=2 + in-depth.
118    Alpha2InDepth,
119    /// α=4 + in-depth.
120    Alpha4InDepth,
121    /// Unspecified / reserved value.
122    Reserved(u8),
123}
124
125impl Hierarchy {
126    /// Human-readable spec label (ETSI EN 300 468 §6.2.13.4 Table 52).
127    #[must_use]
128    pub fn name(self) -> &'static str {
129        match self {
130            Self::NonHierarchicalNative => "non-hierarchical, native interleaver",
131            Self::Alpha1Native => "α=1, native interleaver",
132            Self::Alpha2Native => "α=2, native interleaver",
133            Self::Alpha4Native => "α=4, native interleaver",
134            Self::NonHierarchicalInDepth => "non-hierarchical, in-depth interleaver",
135            Self::Alpha1InDepth => "α=1, in-depth interleaver",
136            Self::Alpha2InDepth => "α=2, in-depth interleaver",
137            Self::Alpha4InDepth => "α=4, in-depth interleaver",
138            Self::Reserved(_) => "reserved",
139        }
140    }
141}
142dvb_common::impl_spec_display!(Hierarchy, Reserved);
143
144/// Convolutional code rate.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize))]
147#[non_exhaustive]
148pub enum CodeRate {
149    /// 1/2.
150    Rate1_2,
151    /// 2/3.
152    Rate2_3,
153    /// 3/4.
154    Rate3_4,
155    /// 5/6.
156    Rate5_6,
157    /// 7/8.
158    Rate7_8,
159    /// Unspecified / reserved value.
160    Reserved(u8),
161}
162
163impl CodeRate {
164    /// Human-readable spec label (ETSI EN 300 468 §6.2.13.4 Table 52).
165    #[must_use]
166    pub fn name(self) -> &'static str {
167        match self {
168            Self::Rate1_2 => "1/2",
169            Self::Rate2_3 => "2/3",
170            Self::Rate3_4 => "3/4",
171            Self::Rate5_6 => "5/6",
172            Self::Rate7_8 => "7/8",
173            Self::Reserved(_) => "reserved",
174        }
175    }
176}
177dvb_common::impl_spec_display!(CodeRate, Reserved);
178
179/// Guard interval fraction.
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181#[cfg_attr(feature = "serde", derive(serde::Serialize))]
182#[non_exhaustive]
183pub enum GuardInterval {
184    /// 1/32.
185    G1_32,
186    /// 1/16.
187    G1_16,
188    /// 1/8.
189    G1_8,
190    /// 1/4.
191    G1_4,
192}
193
194impl GuardInterval {
195    /// Human-readable spec label (ETSI EN 300 468 §6.2.13.4 Table 52).
196    #[must_use]
197    pub fn name(self) -> &'static str {
198        match self {
199            Self::G1_32 => "1/32",
200            Self::G1_16 => "1/16",
201            Self::G1_8 => "1/8",
202            Self::G1_4 => "1/4",
203        }
204    }
205}
206dvb_common::impl_spec_display!(GuardInterval);
207
208/// Transmission mode.
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210#[cfg_attr(feature = "serde", derive(serde::Serialize))]
211#[non_exhaustive]
212pub enum TransmissionMode {
213    /// 2k mode.
214    Mode2k,
215    /// 8k mode.
216    Mode8k,
217    /// 4k mode.
218    Mode4k,
219    /// Unspecified / reserved value.
220    Reserved(u8),
221}
222
223impl TransmissionMode {
224    /// Human-readable spec label (ETSI EN 300 468 §6.2.13.4 Table 52).
225    #[must_use]
226    pub fn name(self) -> &'static str {
227        match self {
228            Self::Mode2k => "2k mode",
229            Self::Mode8k => "8k mode",
230            Self::Mode4k => "4k mode",
231            Self::Reserved(_) => "reserved",
232        }
233    }
234}
235dvb_common::impl_spec_display!(TransmissionMode, Reserved);
236
237/// Terrestrial Delivery System Descriptor.
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239#[cfg_attr(feature = "serde", derive(serde::Serialize))]
240pub struct TerrestrialDeliverySystemDescriptor {
241    /// Centre frequency in units of 10 Hz.
242    pub centre_frequency_10hz: u32,
243    /// Channel bandwidth.
244    pub bandwidth: Bandwidth,
245    /// Spec `priority` bit (EN 300 468 Table 46): `false` = HP (high priority), `true` = LP (low priority).
246    pub priority: bool,
247    /// Time slicing used (spec field polarity: 0 = used → store as bool "used").
248    pub time_slicing_used: bool,
249    /// MPE-FEC used (spec field polarity: 0 = used → store as bool "used").
250    pub mpe_fec_used: bool,
251    /// Constellation.
252    pub constellation: Constellation,
253    /// Hierarchy mode.
254    pub hierarchy: Hierarchy,
255    /// High-priority stream FEC code rate.
256    pub code_rate_hp: CodeRate,
257    /// Low-priority stream FEC code rate (ignored for non-hierarchical).
258    pub code_rate_lp: CodeRate,
259    /// Guard interval fraction.
260    pub guard_interval: GuardInterval,
261    /// Transmission mode (FFT size).
262    pub transmission_mode: TransmissionMode,
263    /// Set when alternative frequencies listed in a frequency_list_descriptor.
264    pub other_frequency_flag: bool,
265}
266
267impl TerrestrialDeliverySystemDescriptor {
268    /// Centre frequency in Hz. The `centre_frequency_10hz` field stores units of
269    /// 10 Hz (EN 300 468 §6.2.13.4), so this conversion is exact.
270    #[must_use]
271    pub fn centre_frequency_hz(&self) -> u64 {
272        u64::from(self.centre_frequency_10hz) * 10
273    }
274
275    /// Set the centre frequency from Hz, encoding to the field's 10 Hz
276    /// resolution (finer precision is truncated).
277    ///
278    /// # Errors
279    /// [`ValueOutOfRange`](crate::Error::ValueOutOfRange) if the value
280    /// exceeds the 32-bit (×10 Hz) field.
281    pub fn set_centre_frequency_hz(&mut self, hz: u64) -> crate::Result<()> {
282        let units = hz / 10;
283        if units > u64::from(u32::MAX) {
284            return Err(crate::Error::ValueOutOfRange {
285                field: "TerrestrialDeliverySystemDescriptor::centre_frequency",
286                reason: "frequency exceeds the 32-bit (10 Hz) field",
287            });
288        }
289        self.centre_frequency_10hz = units as u32;
290        Ok(())
291    }
292}
293
294fn parse_bandwidth(raw: u8) -> Bandwidth {
295    match raw {
296        0 => Bandwidth::Mhz8,
297        1 => Bandwidth::Mhz7,
298        2 => Bandwidth::Mhz6,
299        3 => Bandwidth::Mhz5,
300        other => Bandwidth::Reserved(other),
301    }
302}
303
304fn parse_constellation(raw: u8) -> Constellation {
305    match raw {
306        0 => Constellation::Qpsk,
307        1 => Constellation::Qam16,
308        2 => Constellation::Qam64,
309        other => Constellation::Reserved(other),
310    }
311}
312
313fn parse_hierarchy(raw: u8) -> Hierarchy {
314    match raw {
315        0 => Hierarchy::NonHierarchicalNative,
316        1 => Hierarchy::Alpha1Native,
317        2 => Hierarchy::Alpha2Native,
318        3 => Hierarchy::Alpha4Native,
319        4 => Hierarchy::NonHierarchicalInDepth,
320        5 => Hierarchy::Alpha1InDepth,
321        6 => Hierarchy::Alpha2InDepth,
322        7 => Hierarchy::Alpha4InDepth,
323        other => Hierarchy::Reserved(other),
324    }
325}
326
327fn parse_code_rate(raw: u8) -> CodeRate {
328    match raw {
329        0 => CodeRate::Rate1_2,
330        1 => CodeRate::Rate2_3,
331        2 => CodeRate::Rate3_4,
332        3 => CodeRate::Rate5_6,
333        4 => CodeRate::Rate7_8,
334        other => CodeRate::Reserved(other),
335    }
336}
337
338fn parse_guard_interval(raw: u8) -> GuardInterval {
339    match raw {
340        0 => GuardInterval::G1_32,
341        1 => GuardInterval::G1_16,
342        2 => GuardInterval::G1_8,
343        3 => GuardInterval::G1_4,
344        _ => GuardInterval::G1_32,
345    }
346}
347
348fn parse_transmission_mode(raw: u8) -> TransmissionMode {
349    match raw {
350        0 => TransmissionMode::Mode2k,
351        1 => TransmissionMode::Mode8k,
352        2 => TransmissionMode::Mode4k,
353        other => TransmissionMode::Reserved(other),
354    }
355}
356
357fn serialize_bandwidth(bw: Bandwidth) -> u8 {
358    match bw {
359        Bandwidth::Mhz8 => 0,
360        Bandwidth::Mhz7 => 1,
361        Bandwidth::Mhz6 => 2,
362        Bandwidth::Mhz5 => 3,
363        Bandwidth::Reserved(v) => v,
364    }
365}
366
367fn serialize_constellation(c: Constellation) -> u8 {
368    match c {
369        Constellation::Qpsk => 0,
370        Constellation::Qam16 => 1,
371        Constellation::Qam64 => 2,
372        Constellation::Reserved(v) => v,
373    }
374}
375
376fn serialize_hierarchy(h: Hierarchy) -> u8 {
377    match h {
378        Hierarchy::NonHierarchicalNative => 0,
379        Hierarchy::Alpha1Native => 1,
380        Hierarchy::Alpha2Native => 2,
381        Hierarchy::Alpha4Native => 3,
382        Hierarchy::NonHierarchicalInDepth => 4,
383        Hierarchy::Alpha1InDepth => 5,
384        Hierarchy::Alpha2InDepth => 6,
385        Hierarchy::Alpha4InDepth => 7,
386        Hierarchy::Reserved(v) => v,
387    }
388}
389
390fn serialize_code_rate(cr: CodeRate) -> u8 {
391    match cr {
392        CodeRate::Rate1_2 => 0,
393        CodeRate::Rate2_3 => 1,
394        CodeRate::Rate3_4 => 2,
395        CodeRate::Rate5_6 => 3,
396        CodeRate::Rate7_8 => 4,
397        CodeRate::Reserved(v) => v,
398    }
399}
400
401fn serialize_guard_interval(gi: GuardInterval) -> u8 {
402    match gi {
403        GuardInterval::G1_32 => 0,
404        GuardInterval::G1_16 => 1,
405        GuardInterval::G1_8 => 2,
406        GuardInterval::G1_4 => 3,
407    }
408}
409
410fn serialize_transmission_mode(tm: TransmissionMode) -> u8 {
411    match tm {
412        TransmissionMode::Mode2k => 0,
413        TransmissionMode::Mode8k => 1,
414        TransmissionMode::Mode4k => 2,
415        TransmissionMode::Reserved(v) => v,
416    }
417}
418
419impl<'a> Parse<'a> for TerrestrialDeliverySystemDescriptor {
420    type Error = crate::error::Error;
421    fn parse(bytes: &'a [u8]) -> Result<Self> {
422        let body = descriptor_body(
423            bytes,
424            TAG,
425            "TerrestrialDeliverySystemDescriptor",
426            "unexpected tag for terrestrial_delivery_system_descriptor",
427        )?;
428        if body.len() != BODY_LEN as usize {
429            return Err(Error::InvalidDescriptor {
430                tag: TAG,
431                reason: "body length must equal 11",
432            });
433        }
434
435        let centre_frequency_10hz = u32::from_be_bytes(body[0..4].try_into().unwrap());
436
437        let byte4 = body[4];
438        let bw_raw = (byte4 & BW_MASK) >> BW_SHIFT;
439        let priority = (byte4 & PRIORITY_MASK) != 0;
440        let time_slicing_used = (byte4 & TIME_SLICING_MASK) == 0;
441        let mpe_fec_used = (byte4 & MPE_FEC_MASK) == 0;
442
443        let byte5 = body[5];
444        let constellation_raw = (byte5 & CONSTELLATION_MASK) >> CONSTELLATION_SHIFT;
445        let hierarchy_raw = (byte5 & HIERARCHY_MASK) >> HIERARCHY_SHIFT;
446        let code_rate_hp_raw = byte5 & CODE_RATE_HP_MASK;
447
448        let byte6 = body[6];
449        let code_rate_lp_raw = (byte6 & CODE_RATE_LP_MASK) >> CODE_RATE_LP_SHIFT;
450        let guard_interval_raw = (byte6 & GUARD_INTERVAL_MASK) >> GUARD_INTERVAL_SHIFT;
451        let transmission_mode_raw = (byte6 & TRANSMISSION_MODE_MASK) >> TRANSMISSION_MODE_SHIFT;
452        let other_frequency_flag = (byte6 & OTHER_FREQ_FLAG_MASK) != 0;
453
454        Ok(Self {
455            centre_frequency_10hz,
456            bandwidth: parse_bandwidth(bw_raw),
457            priority,
458            time_slicing_used,
459            mpe_fec_used,
460            constellation: parse_constellation(constellation_raw),
461            hierarchy: parse_hierarchy(hierarchy_raw),
462            code_rate_hp: parse_code_rate(code_rate_hp_raw),
463            code_rate_lp: parse_code_rate(code_rate_lp_raw),
464            guard_interval: parse_guard_interval(guard_interval_raw),
465            transmission_mode: parse_transmission_mode(transmission_mode_raw),
466            other_frequency_flag,
467        })
468    }
469}
470
471impl Serialize for TerrestrialDeliverySystemDescriptor {
472    type Error = crate::error::Error;
473    fn serialized_len(&self) -> usize {
474        HEADER_LEN + BODY_LEN as usize
475    }
476
477    fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
478        let len = self.serialized_len();
479        if buf.len() < len {
480            return Err(Error::OutputBufferTooSmall {
481                need: len,
482                have: buf.len(),
483            });
484        }
485        buf[0] = TAG;
486        buf[1] = BODY_LEN;
487
488        buf[2..6].copy_from_slice(&self.centre_frequency_10hz.to_be_bytes());
489
490        let byte6 = (serialize_bandwidth(self.bandwidth) << BW_SHIFT)
491            | if self.priority { PRIORITY_MASK } else { 0 }
492            | if !self.time_slicing_used {
493                TIME_SLICING_MASK
494            } else {
495                0
496            }
497            | if !self.mpe_fec_used { MPE_FEC_MASK } else { 0 }
498            | RESERVED_FU_MASK;
499        buf[6] = byte6;
500
501        let byte7 = (serialize_constellation(self.constellation) << CONSTELLATION_SHIFT)
502            | (serialize_hierarchy(self.hierarchy) << HIERARCHY_SHIFT)
503            | serialize_code_rate(self.code_rate_hp);
504        buf[7] = byte7;
505
506        let byte8 = (serialize_code_rate(self.code_rate_lp) << CODE_RATE_LP_SHIFT)
507            | (serialize_guard_interval(self.guard_interval) << GUARD_INTERVAL_SHIFT)
508            | (serialize_transmission_mode(self.transmission_mode) << TRANSMISSION_MODE_SHIFT)
509            | if self.other_frequency_flag {
510                OTHER_FREQ_FLAG_MASK
511            } else {
512                0
513            };
514        buf[8] = byte8;
515
516        buf[9..13].copy_from_slice(&TRAILING_RESERVED.to_be_bytes());
517
518        Ok(len)
519    }
520}
521impl<'a> crate::traits::DescriptorDef<'a> for TerrestrialDeliverySystemDescriptor {
522    const TAG: u8 = TAG;
523    const NAME: &'static str = "TERRESTRIAL_DELIVERY_SYSTEM";
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn parse_extracts_centre_frequency_10hz() {
532        let raw = [
533            TAG, BODY_LEN, 0x04, 0xA8, 0x58, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
534        ];
535        let d = TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap();
536        assert_eq!(d.centre_frequency_10hz, 0x04A858F0);
537    }
538
539    #[test]
540    fn parse_extracts_bandwidth_8mhz() {
541        let raw = [
542            TAG, BODY_LEN, 0x04, 0xA8, 0x58, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
543        ];
544        let d = TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap();
545        assert_eq!(d.bandwidth, Bandwidth::Mhz8);
546    }
547
548    #[test]
549    fn parse_extracts_bandwidth_7mhz() {
550        let raw = [
551            TAG,
552            BODY_LEN,
553            0x04,
554            0xA8,
555            0x58,
556            0xF0,
557            (0b001 << BW_SHIFT),
558            0x00,
559            0x00,
560            0x00,
561            0xFF,
562            0xFF,
563            0xFF,
564            0xFF,
565        ];
566        let d = TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap();
567        assert_eq!(d.bandwidth, Bandwidth::Mhz7);
568    }
569
570    #[test]
571    fn parse_extracts_constellation_qam64() {
572        let raw = [
573            TAG,
574            BODY_LEN,
575            0x04,
576            0xA8,
577            0x58,
578            0xF0,
579            0x00,
580            (0b10 << CONSTELLATION_SHIFT),
581            0x00,
582            0xFF,
583            0xFF,
584            0xFF,
585            0xFF,
586        ];
587        let d = TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap();
588        assert_eq!(d.constellation, Constellation::Qam64);
589    }
590
591    #[test]
592    fn parse_extracts_code_rate_hp_and_lp() {
593        let raw = [
594            TAG,
595            BODY_LEN,
596            0x04,
597            0xA8,
598            0x58,
599            0xF0,
600            0x00,
601            0b10 << CONSTELLATION_SHIFT,
602            0b100 << CODE_RATE_LP_SHIFT,
603            0xFF,
604            0xFF,
605            0xFF,
606            0xFF,
607        ];
608        let d = TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap();
609        assert_eq!(d.code_rate_hp, CodeRate::Rate1_2);
610        assert_eq!(d.code_rate_lp, CodeRate::Rate7_8);
611    }
612
613    #[test]
614    fn parse_extracts_guard_interval_1_4() {
615        let raw = [
616            TAG,
617            BODY_LEN,
618            0x04,
619            0xA8,
620            0x58,
621            0xF0,
622            0x00,
623            0x00,
624            0b11 << GUARD_INTERVAL_SHIFT,
625            0xFF,
626            0xFF,
627            0xFF,
628            0xFF,
629        ];
630        let d = TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap();
631        assert_eq!(d.guard_interval, GuardInterval::G1_4);
632    }
633
634    #[test]
635    fn parse_extracts_transmission_mode_8k() {
636        let raw = [
637            TAG,
638            BODY_LEN,
639            0x04,
640            0xA8,
641            0x58,
642            0xF0,
643            0x00,
644            0x00,
645            0b01 << TRANSMISSION_MODE_SHIFT,
646            0xFF,
647            0xFF,
648            0xFF,
649            0xFF,
650        ];
651        let d = TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap();
652        assert_eq!(d.transmission_mode, TransmissionMode::Mode8k);
653    }
654
655    #[test]
656    fn parse_extracts_other_frequency_flag() {
657        let raw = [
658            TAG,
659            BODY_LEN,
660            0x04,
661            0xA8,
662            0x58,
663            0xF0,
664            0x00,
665            0x00,
666            OTHER_FREQ_FLAG_MASK,
667            0xFF,
668            0xFF,
669            0xFF,
670            0xFF,
671        ];
672        let d = TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap();
673        assert!(d.other_frequency_flag);
674    }
675
676    #[test]
677    fn parse_preserves_reserved_bandwidth_in_reserve_variant() {
678        let raw = [
679            TAG,
680            BODY_LEN,
681            0x04,
682            0xA8,
683            0x58,
684            0xF0,
685            (0b111 << BW_SHIFT),
686            0x00,
687            0x00,
688            0x00,
689            0xFF,
690            0xFF,
691            0xFF,
692            0xFF,
693        ];
694        let d = TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap();
695        assert_eq!(d.bandwidth, Bandwidth::Reserved(0b111));
696    }
697
698    #[test]
699    fn parse_rejects_wrong_tag() {
700        let raw = [
701            0x5B, BODY_LEN, 0x04, 0xA8, 0x58, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
702        ];
703        assert!(matches!(
704            TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap_err(),
705            Error::InvalidDescriptor { tag: 0x5B, .. }
706        ));
707    }
708
709    #[test]
710    fn parse_rejects_wrong_length() {
711        let raw = [
712            TAG, 12, 0x04, 0xA8, 0x58, 0xF0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
713        ];
714        assert!(matches!(
715            TerrestrialDeliverySystemDescriptor::parse(&raw).unwrap_err(),
716            Error::InvalidDescriptor { tag: TAG, .. }
717        ));
718    }
719
720    #[test]
721    fn serialize_round_trip_full_set() {
722        let d = TerrestrialDeliverySystemDescriptor {
723            centre_frequency_10hz: 0x04A858F0,
724            bandwidth: Bandwidth::Mhz8,
725            priority: true,
726            time_slicing_used: false,
727            mpe_fec_used: true,
728            constellation: Constellation::Qam64,
729            hierarchy: Hierarchy::Alpha2Native,
730            code_rate_hp: CodeRate::Rate3_4,
731            code_rate_lp: CodeRate::Rate7_8,
732            guard_interval: GuardInterval::G1_4,
733            transmission_mode: TransmissionMode::Mode8k,
734            other_frequency_flag: true,
735        };
736        let mut buf = vec![0u8; d.serialized_len()];
737        d.serialize_into(&mut buf).unwrap();
738        let parsed = TerrestrialDeliverySystemDescriptor::parse(&buf).unwrap();
739        assert_eq!(parsed, d);
740    }
741}