1use crate::descriptors::DescriptorLoop;
13use crate::error::{Error, Result};
14use broadcast_common::{Parse, Serialize};
15
16pub const TABLE_ID: u8 = 0x73;
18pub const PID: u16 = 0x0014;
20
21const HEADER_LEN: usize = 3;
22const UTC_TIME_LEN: usize = 5;
23const DESC_LOOP_LEN_FIELD: usize = 2;
24const CRC_LEN: usize = 4;
25const MIN_SECTION_LEN: usize = HEADER_LEN + UTC_TIME_LEN + DESC_LOOP_LEN_FIELD + CRC_LEN;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize))]
30#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
31pub struct TotSection<'a> {
32 pub(crate) utc_time_raw: [u8; 5],
36 pub descriptors: DescriptorLoop<'a>,
40}
41
42impl<'a> TotSection<'a> {
43 #[must_use]
49 pub fn utc_time_decoded(&self) -> Option<broadcast_common::time::MjdBcdDateTime> {
50 broadcast_common::time::decode_mjd_bcd(self.utc_time_raw)
51 }
52
53 pub fn set_utc_time_decoded(
59 &mut self,
60 dt: broadcast_common::time::MjdBcdDateTime,
61 ) -> Result<()> {
62 self.utc_time_raw =
63 broadcast_common::time::encode_mjd_bcd(dt).ok_or(Error::ValueOutOfRange {
64 field: "TotSection::utc_time",
65 reason: "date not representable in 16-bit MJD",
66 })?;
67 Ok(())
68 }
69
70 #[must_use]
72 pub fn utc_time_raw(&self) -> [u8; 5] {
73 self.utc_time_raw
74 }
75
76 #[must_use]
78 pub fn new(utc_time_raw: [u8; 5], descriptors: DescriptorLoop<'a>) -> Self {
79 Self {
80 utc_time_raw,
81 descriptors,
82 }
83 }
84}
85
86#[cfg(feature = "chrono")]
87impl TotSection<'_> {
88 #[must_use]
93 pub fn utc_time(&self) -> Option<chrono::DateTime<chrono::Utc>> {
94 broadcast_common::time::decode_mjd_bcd_utc(self.utc_time_raw)
95 }
96
97 pub fn set_utc_time(&mut self, utc_time: chrono::DateTime<chrono::Utc>) -> Result<()> {
103 self.utc_time_raw =
104 broadcast_common::time::encode_mjd_bcd_utc(utc_time).ok_or(Error::ValueOutOfRange {
105 field: "TotSection::utc_time",
106 reason: "date not representable in 16-bit MJD",
107 })?;
108 Ok(())
109 }
110}
111
112impl<'a> Parse<'a> for TotSection<'a> {
113 type Error = crate::error::Error;
114 fn parse(bytes: &'a [u8]) -> Result<Self> {
115 let min_len = HEADER_LEN + UTC_TIME_LEN + DESC_LOOP_LEN_FIELD + CRC_LEN;
116 if bytes.len() < min_len {
117 return Err(Error::BufferTooShort {
118 need: min_len,
119 have: bytes.len(),
120 what: "TotSection",
121 });
122 }
123 if bytes[0] != TABLE_ID {
124 return Err(Error::UnexpectedTableId {
125 table_id: bytes[0],
126 what: "TotSection",
127 expected: &[TABLE_ID],
128 });
129 }
130 let section_length = ((bytes[1] & 0x0F) as u16) << 8 | bytes[2] as u16;
131 let total = super::check_section_length(
132 bytes.len(),
133 HEADER_LEN,
134 section_length as usize,
135 MIN_SECTION_LEN,
136 )?;
137 let utc_time_raw = [bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]];
138 let dl_pos = HEADER_LEN + UTC_TIME_LEN;
139 let dl = (((bytes[dl_pos] & 0x0F) as usize) << 8) | bytes[dl_pos + 1] as usize;
140 let d_start = dl_pos + DESC_LOOP_LEN_FIELD;
141 let d_end = d_start + dl;
142 if d_end > total - CRC_LEN {
143 return Err(Error::SectionLengthOverflow {
144 declared: dl,
145 available: (total - CRC_LEN).saturating_sub(d_start),
146 });
147 }
148 Ok(TotSection {
149 utc_time_raw,
150 descriptors: DescriptorLoop::new(&bytes[d_start..d_end]),
151 })
152 }
153}
154
155impl Serialize for TotSection<'_> {
156 type Error = crate::error::Error;
157 fn serialized_len(&self) -> usize {
158 HEADER_LEN + UTC_TIME_LEN + DESC_LOOP_LEN_FIELD + self.descriptors.len() + CRC_LEN
159 }
160 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
161 let len = self.serialized_len();
162 if buf.len() < len {
163 return Err(Error::OutputBufferTooSmall {
164 need: len,
165 have: buf.len(),
166 });
167 }
168 let section_length = (len - HEADER_LEN) as u16;
169 if section_length > 0x0FFF {
170 return Err(Error::SectionLengthOverflow {
171 declared: section_length as usize,
172 available: 0x0FFF,
173 });
174 }
175 buf[0] = TABLE_ID;
176 buf[1] = super::SECTION_B1_FLAGS_SHORT | ((section_length >> 8) as u8 & 0x0F);
179 buf[2] = (section_length & 0xFF) as u8;
180 buf[3..8].copy_from_slice(&self.utc_time_raw);
181 let dl = self.descriptors.len() as u16;
182 buf[8] = 0xF0 | ((dl >> 8) as u8 & 0x0F);
183 buf[9] = (dl & 0xFF) as u8;
184 let d_end = 10 + self.descriptors.len();
185 buf[10..d_end].copy_from_slice(self.descriptors.raw());
186 let crc_pos = len - CRC_LEN;
187 let crc = broadcast_common::crc32_mpeg2::compute(&buf[..crc_pos]);
188 buf[crc_pos..len].copy_from_slice(&crc.to_be_bytes());
189 Ok(len)
190 }
191}
192impl<'a> crate::traits::TableDef<'a> for TotSection<'a> {
193 const TABLE_ID_RANGES: &'static [(u8, u8)] = &[(TABLE_ID, TABLE_ID)];
194 const NAME: &'static str = "TIME_OFFSET";
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 fn build_tot(desc: &[u8]) -> Vec<u8> {
202 let section_length = (UTC_TIME_LEN + DESC_LOOP_LEN_FIELD + desc.len() + CRC_LEN) as u16;
203 let mut v = Vec::new();
204 v.push(TABLE_ID);
205 v.push(0x70 | ((section_length >> 8) as u8 & 0x0F));
207 v.push((section_length & 0xFF) as u8);
208 v.extend_from_slice(&[0xE4, 0x09, 0x12, 0x34, 0x56]);
209 let dl = desc.len() as u16;
210 v.push(0xF0 | ((dl >> 8) as u8 & 0x0F));
211 v.push((dl & 0xFF) as u8);
212 v.extend_from_slice(desc);
213 let crc = broadcast_common::crc32_mpeg2::compute(&v);
214 v.extend_from_slice(&crc.to_be_bytes());
215 v
216 }
217
218 #[test]
219 fn parse_with_no_descriptors() {
220 let bytes = build_tot(&[]);
221 let tot = TotSection::parse(&bytes).unwrap();
222 assert_eq!(tot.utc_time_raw(), [0xE4, 0x09, 0x12, 0x34, 0x56]);
223 assert_eq!(tot.descriptors.raw(), &[] as &[u8]);
224 }
225
226 #[test]
227 fn parse_with_local_time_offset_descriptor() {
228 let lto = [
229 0x58u8, 13, b'G', b'B', b'R', 0x02, 0x00, 0x00, 0xE4, 0x09, 0x12, 0x34, 0x56, 0x01,
230 0x00,
231 ];
232 let bytes = build_tot(<o);
233 let tot = TotSection::parse(&bytes).unwrap();
234 assert_eq!(tot.descriptors.raw(), <o[..]);
235 }
236
237 #[test]
238 fn parse_rejects_wrong_tag() {
239 let mut bytes = build_tot(&[]);
240 bytes[0] = 0x70;
241 assert!(matches!(
242 TotSection::parse(&bytes).unwrap_err(),
243 Error::UnexpectedTableId { table_id: 0x70, .. }
244 ));
245 }
246
247 #[test]
248 fn serialize_round_trip() {
249 let lto = [0x58u8, 0];
250 let bytes = build_tot(<o);
251 let tot = TotSection::parse(&bytes).unwrap();
252 let mut buf = vec![0u8; tot.serialized_len()];
253 tot.serialize_into(&mut buf).unwrap();
254 assert_eq!(buf, bytes, "TOT byte-identity against hand-built input");
255 let re = TotSection::parse(&buf).unwrap();
256 assert_eq!(tot, re);
257 }
258
259 #[test]
260 fn parse_rejects_zero_section_length() {
261 let mut buf = vec![0u8; 64];
262 buf[0] = TABLE_ID;
263 buf[1] = 0xF0;
264 buf[2] = 0x00;
265 for b in &mut buf[3..] {
266 *b = 0xFF;
267 }
268 assert!(matches!(
269 TotSection::parse(&buf).unwrap_err(),
270 Error::SectionLengthOverflow { .. }
271 ));
272 }
273}