dvb_si/section.rs
1//! Generic PSI/SI section framing — ETSI EN 300 468 §5.1.1.
2//!
3//! Every PSI and SI table is carried in one or more sections. This module
4//! parses the **section header** and exposes the payload + CRC for
5//! table-specific parsers to consume.
6//!
7//! # Section layout
8//!
9//! ```text
10//! byte 0: table_id (8 bits)
11//! byte 1 bit 7: section_syntax_indicator (1 bit)
12//! byte 1 bit 6: private_indicator (1 bit)
13//! byte 1 bits 5-4: reserved (2 bits — ignored)
14//! byte 1 bits 3-0 + byte 2: section_length (12 bits)
15//!
16//! Long-form (section_syntax_indicator == 1):
17//! byte 3-4: table_id_extension (16 bits)
18//! byte 5: reserved(2) | version_number(5) | current_next_indicator(1)
19//! byte 6: section_number (8 bits)
20//! byte 7: last_section_number (8 bits)
21//! byte 8..(total-4): payload
22//! last 4 bytes: CRC_32
23//!
24//! Short-form (section_syntax_indicator == 0, e.g. TDT):
25//! byte 3..(3+section_length): payload (no extension header, no CRC)
26//! ```
27//!
28//! NOTE the TOT exception: the TOT (0x73) also sets SSI=0 but DOES end with a
29//! CRC_32 (EN 300 468 §5.2.6). Parsing it through this generic short-form
30//! path folds the CRC into `payload` — use [`crate::tables::tot::Tot`].
31//!
32//! `section_length` counts bytes *after* the 3-byte section header, so the
33//! total section size is `section_length + 3`.
34
35use dvb_common::crc32_mpeg2 as crc;
36use crate::error::{Error, Result};
37use dvb_common::{Parse, Serialize};
38
39// Minimum bytes to read the section header (table_id + section_syntax_indicator
40// + section_length field = 3 bytes).
41const MIN_HEADER_LEN: usize = 3;
42
43// Long-form header adds: extension_id(2) + version/cni(1) + sec_num(1) +
44// last_sec_num(1) = 5 bytes.
45const LONG_FORM_EXTRA: usize = 5;
46
47// CRC occupies the last 4 bytes of every long-form section.
48const CRC_LEN: usize = 4;
49
50/// A parsed PSI/SI section header, borrowing the raw input buffer for payload.
51///
52/// Created via `Section::parse(bytes)`. Does **not** validate the CRC on
53/// construction — call [`Section::validate_crc`] explicitly.
54#[derive(Debug, Clone, PartialEq, Eq)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
56pub struct Section<'a> {
57 /// Table identifier.
58 pub table_id: u8,
59 /// When `true` the section uses the long-form syntax (has extension header
60 /// and CRC). When `false` only the 3-byte header is present (short form,
61 /// e.g. TDT — but see the module docs for the TOT exception: SSI=0 yet
62 /// CRC present; parse TOT via `tables::tot`, not this path).
63 pub section_syntax_indicator: bool,
64 /// Private indicator bit (meaning is table-specific).
65 pub private_indicator: bool,
66 /// Number of bytes following byte 2 of the section header.
67 pub section_length: u16,
68 /// Table ID extension (aka `table_id_extension`). Present only for
69 /// long-form sections; zero for short-form.
70 pub extension_id: u16,
71 /// Version number (5 bits). Present only for long-form sections.
72 pub version_number: u8,
73 /// `current_next_indicator` flag. Present only for long-form sections.
74 pub current_next_indicator: bool,
75 /// Section number within the table sub-table.
76 pub section_number: u8,
77 /// Number of the last section in the table sub-table.
78 pub last_section_number: u8,
79 /// Section payload: excludes the header bytes and the trailing CRC for
80 /// long-form sections. For short-form sections this is bytes
81 /// `3..(section_length + 3)`.
82 #[cfg_attr(feature = "serde", serde(borrow))]
83 pub payload: &'a [u8],
84 /// Declared CRC value (last 4 bytes, big-endian). `None` for short-form
85 /// sections which carry no CRC.
86 pub crc32: Option<u32>,
87}
88
89impl<'a> Section<'a> {
90 /// Return the payload slice (same as the `payload` field — convenience
91 /// getter for code that has a `&Section` reference).
92 #[inline]
93 pub fn payload(&self) -> &'a [u8] {
94 self.payload
95 }
96
97 /// Validate the CRC-32 of the section against `raw` — the complete section
98 /// bytes (including header and CRC suffix).
99 ///
100 /// For short-form sections (`section_syntax_indicator == false`) this
101 /// returns `Ok(())` immediately because no CRC is present.
102 ///
103 /// # Errors
104 ///
105 /// Returns [`Error::CrcMismatch`] when the computed CRC over
106 /// `raw[..raw.len() - 4]` does not match the declared value at
107 /// `raw[raw.len()-4..]`.
108 pub fn validate_crc(&self, raw: &[u8]) -> Result<()> {
109 let expected = match self.crc32 {
110 None => return Ok(()), // short-form — no CRC
111 Some(v) => v,
112 };
113
114 // Guard: raw must be at least CRC_LEN bytes for a valid long-form section.
115 if raw.len() < CRC_LEN {
116 return Err(Error::BufferTooShort {
117 need: CRC_LEN,
118 have: raw.len(),
119 what: "CRC suffix in validate_crc",
120 });
121 }
122
123 // The CRC covers everything up to (but not including) the 4 CRC bytes.
124 let covered = &raw[..raw.len() - CRC_LEN];
125 let computed = crc::compute(covered);
126
127 if computed != expected {
128 return Err(Error::CrcMismatch { computed, expected });
129 }
130 Ok(())
131 }
132}
133
134impl<'a> Parse<'a> for Section<'a> {
135 type Error = crate::error::Error;
136 /// Parse a complete section from `bytes`.
137 ///
138 /// # Errors
139 ///
140 /// - [`Error::BufferTooShort`] — fewer than 3 bytes supplied.
141 /// - [`Error::SectionLengthOverflow`] — `section_length` field declares
142 /// more data than `bytes` contains.
143 fn parse(bytes: &'a [u8]) -> Result<Self> {
144 // ── 3-byte common header ────────────────────────────────────────────
145 if bytes.len() < MIN_HEADER_LEN {
146 return Err(Error::BufferTooShort {
147 need: MIN_HEADER_LEN,
148 have: bytes.len(),
149 what: "section header",
150 });
151 }
152
153 let table_id = bytes[0];
154 let section_syntax_indicator = (bytes[1] & 0x80) != 0;
155 let private_indicator = (bytes[1] & 0x40) != 0;
156 let section_length = (((bytes[1] & 0x0F) as u16) << 8) | (bytes[2] as u16);
157
158 // Total section size is section_length + 3 (the 3-byte header itself
159 // is not counted by section_length).
160 let total = (section_length as usize) + MIN_HEADER_LEN;
161
162 if bytes.len() < total {
163 return Err(Error::SectionLengthOverflow {
164 declared: total,
165 available: bytes.len(),
166 });
167 }
168
169 // Work only inside the declared section boundary.
170 let section_bytes = &bytes[..total];
171
172 if !section_syntax_indicator {
173 // ── Short-form section (e.g. TDT) ──────────────────────────────
174 // No extension header, no CRC. Payload is everything after the
175 // 3-byte header.
176 let payload = §ion_bytes[MIN_HEADER_LEN..];
177 return Ok(Section {
178 table_id,
179 section_syntax_indicator,
180 private_indicator,
181 section_length,
182 extension_id: 0,
183 version_number: 0,
184 current_next_indicator: false,
185 section_number: 0,
186 last_section_number: 0,
187 payload,
188 crc32: None,
189 });
190 }
191
192 // ── Long-form section ───────────────────────────────────────────────
193 // Minimum size for a valid long-form section:
194 // 3 (common header) + 5 (extension header) + 4 (CRC) = 12 bytes.
195 let min_long = MIN_HEADER_LEN + LONG_FORM_EXTRA + CRC_LEN;
196 if section_bytes.len() < min_long {
197 return Err(Error::BufferTooShort {
198 need: min_long,
199 have: section_bytes.len(),
200 what: "long-form section extension header + CRC",
201 });
202 }
203
204 let extension_id = ((bytes[3] as u16) << 8) | (bytes[4] as u16);
205 let version_number = (bytes[5] >> 1) & 0x1F;
206 let current_next_indicator = (bytes[5] & 0x01) != 0;
207 let section_number = bytes[6];
208 let last_section_number = bytes[7];
209
210 // Payload: bytes[8 .. total-4] (excludes 5-byte extension header
211 // counting from offset 3, and the 4-byte CRC at the end).
212 let payload_start = MIN_HEADER_LEN + LONG_FORM_EXTRA;
213 let payload_end = total - CRC_LEN;
214 let payload = §ion_bytes[payload_start..payload_end];
215
216 // Read declared CRC from last 4 bytes of the section (big-endian).
217 let crc_offset = total - CRC_LEN;
218 let crc32 = Some(
219 ((section_bytes[crc_offset] as u32) << 24)
220 | ((section_bytes[crc_offset + 1] as u32) << 16)
221 | ((section_bytes[crc_offset + 2] as u32) << 8)
222 | (section_bytes[crc_offset + 3] as u32),
223 );
224
225 Ok(Section {
226 table_id,
227 section_syntax_indicator,
228 private_indicator,
229 section_length,
230 extension_id,
231 version_number,
232 current_next_indicator,
233 section_number,
234 last_section_number,
235 payload,
236 crc32,
237 })
238 }
239}
240
241impl Serialize for Section<'_> {
242 type Error = crate::error::Error;
243 fn serialized_len(&self) -> usize {
244 // Total size = section_length + 3 (the 3-byte base header precedes
245 // the bytes counted by section_length).
246 usize::from(self.section_length) + MIN_HEADER_LEN
247 }
248
249 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
250 let need = self.serialized_len();
251 if buf.len() < need {
252 return Err(Error::OutputBufferTooSmall {
253 need,
254 have: buf.len(),
255 });
256 }
257
258 // Byte 0: table_id
259 buf[0] = self.table_id;
260
261 // Byte 1: SSI | PI | 2-bit reserved (set high per spec) | length hi 4 bits
262 let length_hi = ((self.section_length >> 8) as u8) & 0x0F;
263 let ssi = u8::from(self.section_syntax_indicator) << 7;
264 let pi = u8::from(self.private_indicator) << 6;
265 // Reserved bits 5..4 are 'reserved' per §5.1.1 — convention is both set.
266 buf[1] = ssi | pi | 0x30 | length_hi;
267
268 // Byte 2: length low 8 bits
269 buf[2] = (self.section_length & 0xFF) as u8;
270
271 if self.section_syntax_indicator {
272 // Long form: 5 bytes of extension header, then payload, then CRC.
273 buf[3] = (self.extension_id >> 8) as u8;
274 buf[4] = (self.extension_id & 0xFF) as u8;
275 // Byte 5: 2-bit reserved (both high) | 5-bit version | 1-bit current_next
276 let version = (self.version_number & 0x1F) << 1;
277 let cni = u8::from(self.current_next_indicator);
278 buf[5] = 0xC0 | version | cni;
279 buf[6] = self.section_number;
280 buf[7] = self.last_section_number;
281
282 let payload_start = MIN_HEADER_LEN + LONG_FORM_EXTRA;
283 let payload_end = payload_start + self.payload.len();
284 buf[payload_start..payload_end].copy_from_slice(self.payload);
285
286 // Append CRC — use the declared value to preserve round-trip identity.
287 let crc = self.crc32.expect("long-form section must carry a CRC");
288 let crc_start = payload_end;
289 buf[crc_start..crc_start + CRC_LEN].copy_from_slice(&crc.to_be_bytes());
290 } else {
291 // Short form: no extension header, no CRC.
292 let payload_end = MIN_HEADER_LEN + self.payload.len();
293 buf[MIN_HEADER_LEN..payload_end].copy_from_slice(self.payload);
294 }
295
296 Ok(need)
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use dvb_common::crc32_mpeg2::compute as crc32;
304
305 // ── Helper: build a minimal long-form section with correct CRC ───────────
306
307 /// Build a syntactically valid long-form section byte vector.
308 ///
309 /// Layout: [table_id, flags+len_hi, len_lo, ext_hi, ext_lo,
310 /// ver_cni, sec_num, last_sec_num, ...payload..., crc(4)]
311 fn make_long_section(
312 table_id: u8,
313 extension_id: u16,
314 version: u8,
315 current_next: bool,
316 section_number: u8,
317 last_section_number: u8,
318 payload: &[u8],
319 ) -> Vec<u8> {
320 // section_length = 5 (extension header) + payload.len() + 4 (CRC)
321 let section_length: u16 = (5 + payload.len() + 4) as u16;
322
323 // reserved(2) | version(5) | current_next(1)
324 let ver_cni = 0xC0u8 | ((version & 0x1F) << 1) | (current_next as u8);
325 let mut buf: Vec<u8> = vec![
326 table_id,
327 // section_syntax_indicator=1, private_indicator=0, reserved=0b11, upper 4 bits of section_length
328 0x80 | 0x30 | ((section_length >> 8) as u8 & 0x0F),
329 (section_length & 0xFF) as u8,
330 (extension_id >> 8) as u8,
331 (extension_id & 0xFF) as u8,
332 ver_cni,
333 section_number,
334 last_section_number,
335 ];
336 buf.extend_from_slice(payload);
337
338 // Compute CRC over bytes so far, append as big-endian u32.
339 let crc = crc32(&buf);
340 buf.push((crc >> 24) as u8);
341 buf.push((crc >> 16) as u8);
342 buf.push((crc >> 8) as u8);
343 buf.push(crc as u8);
344
345 buf
346 }
347
348 // ── Test 1 ───────────────────────────────────────────────────────────────
349
350 #[test]
351 fn parse_rejects_buffer_shorter_than_3_bytes() {
352 for bad_len in [0usize, 1, 2] {
353 let buf = vec![0x00u8; bad_len];
354 let err = Section::parse(&buf).unwrap_err();
355 assert!(
356 matches!(err, Error::BufferTooShort { need: 3, have, .. } if have == bad_len),
357 "expected BufferTooShort for len={bad_len}, got {err:?}"
358 );
359 }
360 }
361
362 // ── Test 2 ───────────────────────────────────────────────────────────────
363
364 #[test]
365 fn parse_reads_table_id_syntax_indicator_and_length() {
366 // Construct a minimal 13-byte long-form section with no payload.
367 // section_length = 5 (extension header) + 0 (payload) + 4 (CRC) = 9
368 // BUT we need section_length >= min for a valid section: 9 → total = 12, which is 12 bytes, not 13.
369 // Let's use 1 byte of payload so section_length = 10, total = 13.
370 let raw = make_long_section(0x42, 0x1234, 3, true, 0, 0, &[0xAB]);
371 assert_eq!(raw.len(), 13);
372
373 let section = Section::parse(&raw).unwrap();
374 assert_eq!(section.table_id, 0x42);
375 assert!(section.section_syntax_indicator);
376 // section_length = 5 + 1 + 4 = 10
377 assert_eq!(section.section_length, 10);
378 }
379
380 // ── Test 3 ───────────────────────────────────────────────────────────────
381
382 #[test]
383 fn parse_rejects_when_section_length_exceeds_buffer() {
384 // Build a 3-byte header that claims section_length = 100 bytes of data.
385 // Buffer is only 3 bytes (just the header), so total = 103 > 3.
386 let buf = [
387 0x00u8, // table_id
388 0x80u8, // section_syntax_indicator=1, section_length upper nibble = 0
389 100u8, // section_length lower byte = 100 → total = 103
390 ];
391 let err = Section::parse(&buf).unwrap_err();
392 assert!(
393 matches!(
394 err,
395 Error::SectionLengthOverflow {
396 declared: 103,
397 available: 3
398 }
399 ),
400 "expected SectionLengthOverflow, got {err:?}"
401 );
402 }
403
404 // ── Test 4 ───────────────────────────────────────────────────────────────
405
406 #[test]
407 fn parse_reads_extension_id_version_current_next_section_numbers() {
408 let raw = make_long_section(
409 0x02, // PMT table_id
410 0xBEEF, // extension_id / program_number
411 7, // version_number
412 true, // current_next
413 2, // section_number
414 5, // last_section_number
415 &[0x00, 0x00], // dummy payload
416 );
417
418 let section = Section::parse(&raw).unwrap();
419 assert_eq!(section.extension_id, 0xBEEF);
420 assert_eq!(section.version_number, 7);
421 assert!(section.current_next_indicator);
422 assert_eq!(section.section_number, 2);
423 assert_eq!(section.last_section_number, 5);
424 }
425
426 #[test]
427 fn parse_reads_current_next_indicator_false() {
428 // Same as test 4 but with current_next = false (bit 0 of byte 5 cleared).
429 let raw = make_long_section(
430 0x02, // PMT table_id
431 0xBEEF, // extension_id
432 7, // version_number
433 false, // current_next — the field under test
434 2, // section_number
435 5, // last_section_number
436 &[0x00, 0x00],
437 );
438
439 let section = Section::parse(&raw).unwrap();
440 assert!(!section.current_next_indicator);
441 // Confirm the other fields are unaffected by flipping bit 0.
442 assert_eq!(section.extension_id, 0xBEEF);
443 assert_eq!(section.version_number, 7);
444 assert_eq!(section.section_number, 2);
445 assert_eq!(section.last_section_number, 5);
446 }
447
448 // ── Test 5 ───────────────────────────────────────────────────────────────
449
450 #[test]
451 fn payload_slice_excludes_header_and_crc() {
452 let inner_payload = &[0x01u8, 0x02, 0x03, 0x04, 0x05];
453 let raw = make_long_section(0x42, 0x0001, 0, true, 0, 0, inner_payload);
454
455 let section = Section::parse(&raw).unwrap();
456 assert_eq!(section.payload(), inner_payload);
457 }
458
459 // ── Test 6 ───────────────────────────────────────────────────────────────
460
461 #[test]
462 fn validate_crc_accepts_matching_crc32() {
463 let raw = make_long_section(0x00, 0x0001, 1, true, 0, 0, &[0xDE, 0xAD, 0xBE, 0xEF]);
464 let section = Section::parse(&raw).unwrap();
465 section.validate_crc(&raw).expect("CRC should match");
466 }
467
468 // ── Test 7 ───────────────────────────────────────────────────────────────
469
470 #[test]
471 fn validate_crc_rejects_flipped_bit() {
472 let mut raw = make_long_section(0x00, 0x0001, 1, true, 0, 0, &[0xDE, 0xAD, 0xBE, 0xEF]);
473 // Flip a bit inside the payload (byte 8 is the first payload byte) BEFORE
474 // parsing. The 4-byte CRC at the tail of `raw` was computed over the
475 // original (un-flipped) bytes and is NOT updated here, so after the flip:
476 // • `raw[..raw.len()-4]` contains corrupted data, and
477 // • `raw[raw.len()-4..]` still holds the CRC of the original data.
478 // Parsing after the flip captures that old CRC into `section.crc32`.
479 // `validate_crc` then recomputes over the corrupted bytes and detects the
480 // mismatch — which is exactly the invariant we are testing.
481 // (Parsing before the flip would immutably borrow `raw` for the lifetime
482 // of `section` — the compiler would then reject `raw[8] ^= 0x01` because
483 // a mutable borrow conflicts with any live shared borrow.)
484 raw[8] ^= 0x01;
485
486 let section = Section::parse(&raw).unwrap();
487 let err = section.validate_crc(&raw).unwrap_err();
488 assert!(
489 matches!(err, Error::CrcMismatch { .. }),
490 "expected CrcMismatch, got {err:?}"
491 );
492 }
493
494 // ── Test (Fix 1 TDD) ─────────────────────────────────────────────────────
495
496 #[test]
497 fn validate_crc_rejects_raw_slice_shorter_than_crc_len() {
498 let raw = make_long_section(0x42, 0x0001, 0, true, 0, 0, &[0xDE, 0xAD]);
499 let section = Section::parse(&raw).unwrap();
500 // Pass an empty slice — shorter than CRC_LEN bytes.
501 let err = section.validate_crc(&[]).unwrap_err();
502 assert!(
503 matches!(err, Error::BufferTooShort { need: CRC_LEN, .. }),
504 "expected BufferTooShort(need=CRC_LEN), got {err:?}"
505 );
506 }
507
508 // ── Test 8 ───────────────────────────────────────────────────────────────
509
510 #[test]
511 fn short_form_section_has_no_crc() {
512 // TDT-style short-form section: section_syntax_indicator = 0.
513 // section_length = 5 (5 bytes of payload), total = 8 bytes.
514 let buf = [
515 0x70u8, // table_id (TDT)
516 0x70u8, // SSI=0, private=1, reserved=0b11, upper nibble of section_length=0
517 0x05u8, // section_length = 5
518 // 5 bytes of "UTC time" payload
519 0xE0, 0x00, 0x00, 0x00, 0x00,
520 ];
521
522 let section = Section::parse(&buf).unwrap();
523 assert!(!section.section_syntax_indicator);
524 assert!(section.crc32.is_none());
525 // validate_crc on short-form should return Ok(()) vacuously.
526 section.validate_crc(&buf).expect("short-form: no CRC to validate");
527 // Payload is the 5 bytes after the 3-byte header.
528 assert_eq!(section.payload(), &buf[3..]);
529 }
530}