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