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