Skip to main content

dvb_vbi/
line_header.rs

1//! Shared first-byte bit-packing for the Teletext / VPS / WSS / CC data units —
2//! ETSI EN 301 775 §4.5.1, §4.6.1, §4.7.1, §4.8.1 (Tables 4, 6, 8, 10).
3//!
4//! Each of these data fields begins with the same first byte (MSB→LSB):
5//! `[7:6]` reserved_future_use = `11`, `[5]` field_parity, `[4:0]` line_offset.
6//! (The monochrome data unit, §4.9.1, packs its first byte differently — two
7//! segment flags occupy `[7:6]` instead — and so does *not* use this header.)
8
9use crate::error::{Error, Result};
10
11/// Size in bytes of the shared first-byte line header.
12pub const LINE_HEADER_LEN: usize = 1;
13
14/// The fixed 2-bit `reserved_future_use` prefix (`11`) occupying bits `[7:6]`
15/// of the header byte.
16pub const RESERVED_PREFIX: u8 = 0b11;
17
18/// The shared first byte of the Teletext / VPS / WSS / CC data fields:
19/// a fixed `reserved_future_use` = `11` prefix, then `field_parity` and a 5-bit
20/// `line_offset` (ETSI EN 301 775 §4.5.1 et al.).
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize))]
23pub struct LineHeader {
24    /// `field_parity` (1 bit): `1` = first field of a frame, `0` = second field.
25    pub field_parity: bool,
26    /// `line_offset` (5 bits): the VBI line, coded per the unit's line_offset
27    /// table. Range `0..=31`.
28    pub line_offset: u8,
29}
30
31impl LineHeader {
32    /// Construct a header from its fields.
33    pub fn new(field_parity: bool, line_offset: u8) -> Self {
34        LineHeader {
35            field_parity,
36            line_offset,
37        }
38    }
39
40    /// Decode the header from a single byte. The `reserved_future_use` prefix
41    /// bits `[7:6]` are not validated (decoders ignore RFU); only the typed
42    /// fields are extracted.
43    pub fn from_byte(byte: u8) -> Self {
44        LineHeader {
45            field_parity: (byte & 0b0010_0000) != 0,
46            line_offset: byte & 0b0001_1111,
47        }
48    }
49
50    /// Encode the header to its single wire byte: `11` | field_parity |
51    /// line_offset. Errors if `line_offset` does not fit in 5 bits.
52    pub fn to_byte(self) -> Result<u8> {
53        if self.line_offset > 0b0001_1111 {
54            return Err(Error::FieldTooWide {
55                what: "line_offset",
56                value: self.line_offset as u32,
57                bits: 5,
58            });
59        }
60        let mut b = RESERVED_PREFIX << 6;
61        if self.field_parity {
62            b |= 0b0010_0000;
63        }
64        b |= self.line_offset;
65        Ok(b)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn round_trip_all_bytes() {
75        // The header byte decodes/encodes losslessly for any input whose RFU
76        // prefix is the canonical `11` (the only value the encoder emits).
77        for raw in 0u16..=0xFF {
78            let raw = raw as u8;
79            let h = LineHeader::from_byte(raw);
80            let re = h.to_byte().unwrap();
81            // Re-encode forces the canonical RFU prefix `11`; compare the
82            // non-RFU bits only.
83            assert_eq!(re & 0b0011_1111, raw & 0b0011_1111, "raw={raw:#04X}");
84            assert_eq!(re >> 6, RESERVED_PREFIX);
85        }
86    }
87
88    #[test]
89    fn field_split() {
90        // 0xD0 = 11 0 10000 -> parity=0, line_offset=16 (a VPS header).
91        let h = LineHeader::from_byte(0xD0);
92        assert!(!h.field_parity);
93        assert_eq!(h.line_offset, 16);
94        assert_eq!(h.to_byte().unwrap(), 0xD0);
95
96        // parity=1, line_offset=21 (a CC first-field header) -> 11 1 10101 = 0xF5.
97        let h = LineHeader::new(true, 21);
98        assert_eq!(h.to_byte().unwrap(), 0xF5);
99    }
100
101    #[test]
102    fn rejects_overwide_line_offset() {
103        assert!(matches!(
104            LineHeader::new(true, 32).to_byte(),
105            Err(Error::FieldTooWide { .. })
106        ));
107    }
108}