Skip to main content

rmt_flute/
flute.rs

1//! FLUTE — File Delivery over Unidirectional Transport (RFC 6726 §3.4).
2//!
3//! FLUTE is built on ALC ([`crate::alc`]) + LCT ([`crate::lct`]). It adds two
4//! fixed-length LCT header extensions — **EXT_FDT** (HET 192) and **EXT_CENC**
5//! (HET 193) — and the **TOI = 0** convention for carrying FDT Instances.
6//!
7//! ⚠ The FDT Instance body itself is an **XML document** and is out of scope of
8//! this binary crate (RFC 6726 §3.4.2): it rides as the encoding-symbol payload
9//! after the FEC Payload ID. This module covers only the binary LCT/ALC framing
10//! extensions; expose the payload bytes and parse the XML in a separate layer.
11
12use crate::error::{Error, Result};
13use crate::ext::HeaderExtension;
14
15/// HET for EXT_FDT (FDT Instance Header) — RFC 6726 §3.4.1. Fixed-length.
16pub const HET_EXT_FDT: u8 = 192;
17/// HET for EXT_CENC (FDT Instance Content Encoding) — RFC 6726 §3.4.3. Fixed.
18pub const HET_EXT_CENC: u8 = 193;
19
20/// The reserved TOI value for FDT Instances (RFC 6726 §3.3). FDT Instances are
21/// carried in ALC packets with TOI = 0.
22pub const TOI_FDT: u32 = 0;
23
24/// FLUTE version carried in EXT_FDT's `V` field (RFC 6726 = 2).
25pub const FLUTE_VERSION: u8 = 2;
26
27/// Maximum FDT Instance ID (20-bit field).
28pub const FDT_INSTANCE_ID_MAX: u32 = (1 << 20) - 1;
29
30/// EXT_FDT — FDT Instance Header (RFC 6726 §3.4.1, HET = 192, fixed-length).
31///
32/// Layout (one 32-bit word): `HET(8) | V(4) | FDT Instance ID(20)`.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[cfg_attr(feature = "serde", derive(serde::Serialize))]
35pub struct ExtFdt {
36    /// FLUTE version (`V`, 4 bits). RFC 6726 = [`FLUTE_VERSION`] (2).
37    pub version: u8,
38    /// FDT Instance ID (20 bits) — identifies the FDT Instance in the session.
39    pub instance_id: u32,
40}
41
42impl ExtFdt {
43    /// Decode from the 3 content bytes of a fixed-length [`HeaderExtension`]
44    /// whose HET is [`HET_EXT_FDT`] (the 24 bits after the HET byte).
45    pub fn parse(content: &[u8]) -> Result<Self> {
46        if content.len() != 3 {
47            return Err(Error::BufferTooShort {
48                need: 3,
49                have: content.len(),
50                what: "EXT_FDT content",
51            });
52        }
53        // 24 bits: V(4) | instance_id(20).
54        let v = content[0] >> 4;
55        let instance_id =
56            ((content[0] as u32 & 0x0F) << 16) | ((content[1] as u32) << 8) | content[2] as u32;
57        Ok(ExtFdt {
58            version: v,
59            instance_id,
60        })
61    }
62
63    /// Encode the 3 content bytes (the 24 bits after HET).
64    pub fn to_content(&self) -> Result<[u8; 3]> {
65        if self.version > 0x0F {
66            return Err(Error::FieldTooWide {
67                what: "EXT_FDT V",
68                value: self.version as u64,
69                bits: 4,
70            });
71        }
72        if self.instance_id > FDT_INSTANCE_ID_MAX {
73            return Err(Error::FieldTooWide {
74                what: "FDT Instance ID",
75                value: self.instance_id as u64,
76                bits: 20,
77            });
78        }
79        Ok([
80            (self.version << 4) | ((self.instance_id >> 16) as u8 & 0x0F),
81            (self.instance_id >> 8) as u8,
82            self.instance_id as u8,
83        ])
84    }
85
86    /// Build a fixed-length [`HeaderExtension`] (HET = 192) for this EXT_FDT,
87    /// writing the 3 content bytes into `scratch`.
88    pub fn to_extension<'a>(&self, scratch: &'a mut [u8; 3]) -> Result<HeaderExtension<'a>> {
89        *scratch = self.to_content()?;
90        Ok(HeaderExtension::new(HET_EXT_FDT, &scratch[..]))
91    }
92}
93
94/// Content-encoding algorithm of an FDT Instance payload (RFC 6726 §3.4.3).
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize))]
97#[non_exhaustive]
98pub enum CencAlgorithm {
99    /// 0 — null (no content encoding).
100    Null,
101    /// 1 — ZLIB (RFC 1950).
102    Zlib,
103    /// 2 — DEFLATE (RFC 1951).
104    Deflate,
105    /// 3 — GZIP (RFC 1952).
106    Gzip,
107    /// Any other (unassigned) value.
108    Other(u8),
109}
110
111impl CencAlgorithm {
112    /// Decode a CENC algorithm byte.
113    pub fn from_u8(v: u8) -> Self {
114        match v {
115            0 => CencAlgorithm::Null,
116            1 => CencAlgorithm::Zlib,
117            2 => CencAlgorithm::Deflate,
118            3 => CencAlgorithm::Gzip,
119            other => CencAlgorithm::Other(other),
120        }
121    }
122
123    /// The wire byte for this algorithm.
124    pub fn to_u8(self) -> u8 {
125        match self {
126            CencAlgorithm::Null => 0,
127            CencAlgorithm::Zlib => 1,
128            CencAlgorithm::Deflate => 2,
129            CencAlgorithm::Gzip => 3,
130            CencAlgorithm::Other(v) => v,
131        }
132    }
133
134    /// Spec label.
135    pub fn name(&self) -> &'static str {
136        match self {
137            CencAlgorithm::Null => "null",
138            CencAlgorithm::Zlib => "ZLIB",
139            CencAlgorithm::Deflate => "DEFLATE",
140            CencAlgorithm::Gzip => "GZIP",
141            CencAlgorithm::Other(_) => "reserved",
142        }
143    }
144}
145
146broadcast_common::impl_spec_display!(CencAlgorithm, Other);
147
148/// EXT_CENC — FDT Instance Content Encoding Header (RFC 6726 §3.4.3, HET = 193,
149/// fixed-length).
150///
151/// Layout (one 32-bit word): `HET(8) | CENC(8) | Reserved(16)`.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize))]
154pub struct ExtCenc {
155    /// Content-encoding algorithm of the FDT Instance payload.
156    pub algorithm: CencAlgorithm,
157}
158
159impl ExtCenc {
160    /// Decode from the 3 content bytes of a fixed-length [`HeaderExtension`]
161    /// whose HET is [`HET_EXT_CENC`]: `CENC(8) | Reserved(16)`.
162    pub fn parse(content: &[u8]) -> Result<Self> {
163        if content.len() != 3 {
164            return Err(Error::BufferTooShort {
165                need: 3,
166                have: content.len(),
167                what: "EXT_CENC content",
168            });
169        }
170        // Reserved 16 bits MUST be 0 (ignored on reception, but we surface it).
171        Ok(ExtCenc {
172            algorithm: CencAlgorithm::from_u8(content[0]),
173        })
174    }
175
176    /// Encode the 3 content bytes (`CENC | Reserved=0`).
177    pub fn to_content(&self) -> [u8; 3] {
178        [self.algorithm.to_u8(), 0, 0]
179    }
180
181    /// Build a fixed-length [`HeaderExtension`] (HET = 193) for this EXT_CENC,
182    /// writing the 3 content bytes into `scratch`.
183    pub fn to_extension<'a>(&self, scratch: &'a mut [u8; 3]) -> HeaderExtension<'a> {
184        *scratch = self.to_content();
185        HeaderExtension::new(HET_EXT_CENC, &scratch[..])
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use alloc::string::ToString;
193
194    #[test]
195    fn ext_fdt_round_trip() {
196        let f = ExtFdt {
197            version: FLUTE_VERSION,
198            instance_id: 0x0_ABCD,
199        };
200        let c = f.to_content().unwrap();
201        // V=2 (0x2_), instance_id high nibble = 0x0 -> 0x20; then 0xAB, 0xCD.
202        assert_eq!(c, [0x20, 0xAB, 0xCD]);
203        assert_eq!(ExtFdt::parse(&c).unwrap(), f);
204
205        // As an extension: HET=192 (fixed), 4 bytes total.
206        let mut scratch = [0u8; 3];
207        let ext = f.to_extension(&mut scratch).unwrap();
208        assert_eq!(ext.het, HET_EXT_FDT);
209        assert!(ext.is_fixed());
210        assert_eq!(ext.serialized_len(), 4);
211    }
212
213    #[test]
214    fn ext_fdt_max_instance_id() {
215        let f = ExtFdt {
216            version: 2,
217            instance_id: FDT_INSTANCE_ID_MAX,
218        };
219        let c = f.to_content().unwrap();
220        assert_eq!(c, [0x2F, 0xFF, 0xFF]);
221        assert_eq!(ExtFdt::parse(&c).unwrap(), f);
222    }
223
224    #[test]
225    fn ext_fdt_rejects_overwide_instance_id() {
226        let f = ExtFdt {
227            version: 2,
228            instance_id: FDT_INSTANCE_ID_MAX + 1,
229        };
230        assert!(matches!(f.to_content(), Err(Error::FieldTooWide { .. })));
231    }
232
233    #[test]
234    fn ext_cenc_round_trip() {
235        for algo in [
236            CencAlgorithm::Null,
237            CencAlgorithm::Zlib,
238            CencAlgorithm::Deflate,
239            CencAlgorithm::Gzip,
240            CencAlgorithm::Other(7),
241        ] {
242            let e = ExtCenc { algorithm: algo };
243            let c = e.to_content();
244            assert_eq!(c[1..], [0, 0]);
245            assert_eq!(ExtCenc::parse(&c).unwrap(), e);
246        }
247        assert_eq!(CencAlgorithm::Gzip.to_string(), "GZIP");
248        assert_eq!(CencAlgorithm::Other(7).to_string(), "reserved(0x07)");
249    }
250}