broadcast-common 10.0.0

Foundation for the rust-broadcast crates: symmetric Parse/Serialize traits, CRC-32/MPEG-2, BCD + MJD time codecs, bit I/O, 33-bit/PCR clock arithmetic, checked field widths, container-mux and Stage traits. no_std + alloc.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
//! Legal-duplicate transport-stream-packet detection — ITU-T H.222.0
//! (08/2023) / ISO/IEC 13818-1 §2.4.3.3.
//!
//! Cited via `mpeg-ts/docs/README.md` (§2.4.3.3 — duplicate packets), which
//! carries the verbatim clause (Rec. ITU-T H.222.0 (08/2023) §2.4.3.3, PDF
//! p. 48):
//!
//! > In transport streams, duplicate packets may be sent as two, and only
//! > two, consecutive transport stream packets of the same PID. The
//! > duplicate packets shall have the same continuity_counter value as the
//! > original packet and the adaptation_field_control field shall be equal
//! > to '01' or '11'. In duplicate packets each byte of the original packet
//! > shall be duplicated, with the exception that in the program clock
//! > reference fields, if present, a valid value shall be encoded.
//!
//! Three independent properties fall out of that sentence:
//!
//! 1. **"each byte of the original packet"**, with the PCR fields the
//!    *sole* exception — a payload-only comparison is too lenient. A packet
//!    differing in, say, `splice_countdown` or OPCR is NOT a legal
//!    duplicate. [`is_legal_duplicate_pair`] implements exactly this byte
//!    comparison.
//! 2. **`adaptation_field_control` must be `01` or `11`** — a byte-identical
//!    repeat of a non-payload-bearing packet (`00`/`10`) is not a "duplicate"
//!    in the spec's sense at all.
//! 3. **"two, and only two"** — a third consecutive repeat is an error
//!    regardless of byte-identity. This is necessarily stateful (a pairwise
//!    comparison alone cannot see the run length), so it is exposed via
//!    [`check_duplicate`], which takes whether the *previous* transition in
//!    the run already consumed the one legal repeat.
//!
//! This module was extracted (issue #956 follow-up) from three independent,
//! disagreeing hand-rolled copies of this rule in `dvb-conformance`,
//! `media-doctor` and `ts-fix`.

/// Byte offset of `adaptation_field_control` (bits `[5:4]`) and
/// `continuity_counter` (bits `[3:0]`) within a raw TS packet — ITU-T
/// H.222.0 (08/2023) §2.4.3.3, Table 2-5.
const AFC_BYTE: usize = 3;

/// `adaptation_field_control` bit indicating a payload is present (`01` or
/// `11`) — H.222.0 Table 2-5.
const PAYLOAD_FLAG: u8 = 0x10;

/// `adaptation_field_control` bit indicating an adaptation field is present
/// (`10` or `11`) — H.222.0 Table 2-5.
const ADAPTATION_FLAG: u8 = 0x20;

/// Byte offset of `adaptation_field_length` within a raw TS packet —
/// H.222.0 §2.4.3.4.
const AF_LEN_BYTE: usize = 4;

/// Byte offset of the adaptation-field flags byte (the byte immediately
/// after `adaptation_field_length`) within a raw TS packet — H.222.0
/// §2.4.3.4 Table 2-6.
const AF_FLAGS_BYTE: usize = 5;

/// Adaptation-field flags-byte bit for `PCR_flag` — H.222.0 §2.4.3.4
/// Table 2-6.
const PCR_FLAG: u8 = 0x10;

/// Byte offset (relative to the start of the packet) of the first PCR byte,
/// when `PCR_flag` is set — immediately after the flags byte, H.222.0
/// §2.4.3.5.
const PCR_FIELD_START: usize = AF_FLAGS_BYTE + 1;

/// Encoded PCR field width: 33-bit `program_clock_reference_base` + 6
/// reserved bits + 9-bit `program_clock_reference_extension` = 48 bits —
/// H.222.0 §2.4.3.5.
const PCR_FIELD_LEN: usize = 6;

/// Minimum `adaptation_field_length` for a `PCR_flag`-bearing adaptation
/// field: the flags byte itself (1) plus the 6-byte PCR field — H.222.0
/// §2.4.3.4 Table 2-6 / §2.4.3.5. `adaptation_field_length` counts bytes
/// after the length byte, starting with the flags byte, so a field that
/// claims `PCR_flag` but declares fewer than this many bytes does not
/// actually reach the PCR — whatever follows the flags byte within the
/// packet is unrelated payload, not the PCR exception.
const MIN_AF_LEN_WITH_PCR: u8 = 1 + PCR_FIELD_LEN as u8;

/// Locate the PCR field's byte range within `pkt`, if one is present.
///
/// Returns `None` when the packet carries no adaptation field, an empty
/// adaptation field, no `PCR_flag`, or is too short to hold the field it
/// claims to have — any of which means there is no PCR exception to apply.
fn pcr_field_range(pkt: &[u8]) -> Option<(usize, usize)> {
    if pkt.len() <= AF_FLAGS_BYTE {
        return None;
    }
    if pkt[AFC_BYTE] & ADAPTATION_FLAG == 0 {
        return None;
    }
    let af_len = pkt[AF_LEN_BYTE];
    if af_len < MIN_AF_LEN_WITH_PCR {
        return None;
    }
    if pkt[AF_FLAGS_BYTE] & PCR_FLAG == 0 {
        return None;
    }
    let end = PCR_FIELD_START + PCR_FIELD_LEN;
    if end > pkt.len() {
        return None;
    }
    Some((PCR_FIELD_START, end))
}

/// Whether `curr` is a legal §2.4.3.3 duplicate of `prev`, ignoring the
/// "two, and only two" cardinality rule (see [`check_duplicate`] for that).
///
/// `prev`/`curr` are raw TS-packet bytes — any equal length of at least 4
/// bytes (the usual case is the fixed 188-byte packet, but this makes no
/// assumption about total packet size). Differing lengths are never a
/// duplicate.
///
/// A duplicate requires, per the spec text quoted at the module level:
/// - `curr`'s `adaptation_field_control` indicates a payload (`01` or
///   `11`) — checking only `curr` is sufficient because the byte-for-byte
///   comparison below (which includes byte 3, the AFC/CC byte) forces
///   `prev`'s AFC bits to match whenever this function returns `true`;
/// - every byte of `prev` and `curr` is identical, with the sole exception
///   of the 6-byte PCR field (located from `curr`'s own adaptation-field
///   flags, which the same byte-3/byte-4/byte-5 equality requirement above
///   forces to agree with `prev`'s).
pub fn is_legal_duplicate_pair(prev: &[u8], curr: &[u8]) -> bool {
    if prev.len() != curr.len() || prev.len() <= AFC_BYTE {
        return false;
    }
    if curr[AFC_BYTE] & PAYLOAD_FLAG == 0 {
        // Property 2: adaptation_field_control must be '01' or '11'.
        return false;
    }

    let pcr_range = pcr_field_range(curr);

    for i in 0..prev.len() {
        let in_pcr_field = matches!(pcr_range, Some((start, end)) if i >= start && i < end);
        if !in_pcr_field && prev[i] != curr[i] {
            // Property 1: every byte identical, PCR fields excepted.
            return false;
        }
    }
    true
}

/// Outcome of [`check_duplicate`]: whether `curr` legally continues (or
/// illegally over-continues) a duplicate run started by `prev`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DuplicateVerdict {
    /// `curr` is not a legal duplicate of `prev` at all — either
    /// `adaptation_field_control` was not `01`/`11`, or some byte other
    /// than the PCR field differs. Callers should fall through to their
    /// normal continuity-counter check.
    NotDuplicate,
    /// `curr` is the single legal repeat of `prev` — §2.4.3.3 permits
    /// exactly one.
    Legal,
    /// `curr` is byte-identical (PCR excepted) to `prev`, but the caller
    /// signalled that the *previous* transition already consumed the one
    /// legal repeat (`dup_already_used == true`). Per "two, and only two
    /// consecutive" packets, a third consecutive repeat is itself a
    /// continuity fault.
    IllegalThirdRepeat,
}

impl DuplicateVerdict {
    /// Static label for this verdict (issue #204 convention).
    pub fn name(&self) -> &'static str {
        match self {
            Self::NotDuplicate => "not duplicate",
            Self::Legal => "legal",
            Self::IllegalThirdRepeat => "illegal third repeat",
        }
    }
}

crate::impl_spec_display!(DuplicateVerdict);

/// Check whether `curr` is a legal duplicate of `prev`, folding in the
/// "two, and only two" cardinality rule via `dup_already_used`: the
/// caller's record of whether the *previous* packet on this PID was
/// already accepted as the one legal repeat of ITS predecessor.
///
/// Typical per-PID caller state is just this one `bool`, reset to `false`
/// whenever [`DuplicateVerdict::NotDuplicate`] is returned (a fresh
/// original packet has been seen) and set to `true` after
/// [`DuplicateVerdict::Legal`].
pub fn check_duplicate(prev: &[u8], curr: &[u8], dup_already_used: bool) -> DuplicateVerdict {
    if !is_legal_duplicate_pair(prev, curr) {
        return DuplicateVerdict::NotDuplicate;
    }
    if dup_already_used {
        DuplicateVerdict::IllegalThirdRepeat
    } else {
        DuplicateVerdict::Legal
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// 188-byte packet, sync=0x47, given PID, AFC/CC byte, and a payload
    /// filled with a repeating pattern. No adaptation field.
    fn payload_packet(pid: u16, afc_cc: u8, fill: u8) -> [u8; 188] {
        let mut pkt = [fill; 188];
        pkt[0] = 0x47;
        pkt[1] = ((pid >> 8) as u8) & 0x1F;
        pkt[2] = (pid & 0xFF) as u8;
        pkt[3] = afc_cc;
        pkt
    }

    /// Packet with an adaptation field carrying PCR (+ optionally OPCR and
    /// splice_countdown), followed by payload filled with `fill`.
    ///
    /// Layout: `[sync][pid_hi][pid_lo][afc_cc][af_len][flags][PCR x6]
    /// [OPCR x6 if opcr][splice_countdown if splice]...[payload]`.
    fn packet_with_adaptation(
        pid: u16,
        afc_cc: u8,
        pcr: [u8; 6],
        opcr: Option<[u8; 6]>,
        splice_countdown: Option<u8>,
        fill: u8,
    ) -> [u8; 188] {
        let mut pkt = [fill; 188];
        pkt[0] = 0x47;
        pkt[1] = ((pid >> 8) as u8) & 0x1F;
        pkt[2] = (pid & 0xFF) as u8;
        pkt[3] = afc_cc | ADAPTATION_FLAG;

        let mut flags = PCR_FLAG;
        let mut body_len = 1 + 6; // flags byte + PCR
        if opcr.is_some() {
            flags |= 0x08; // OPCR_flag
            body_len += 6;
        }
        if splice_countdown.is_some() {
            flags |= 0x04; // splicing_point_flag
            body_len += 1;
        }

        pkt[4] = body_len as u8; // adaptation_field_length
        pkt[5] = flags;
        pkt[6..12].copy_from_slice(&pcr);
        let mut cursor = 12usize;
        if let Some(o) = opcr {
            pkt[cursor..cursor + 6].copy_from_slice(&o);
            cursor += 6;
        }
        if let Some(sc) = splice_countdown {
            pkt[cursor] = sc;
            cursor += 1;
        }
        // Rest of packet after the adaptation field is payload (already
        // filled with `fill` from the initial array construction);
        // `cursor` marks where it starts, which callers may mutate.
        let _ = cursor;
        pkt
    }

    #[test]
    fn byte_identical_is_legal() {
        let a = payload_packet(0x0100, 0x10, 0xAB);
        let b = a;
        assert!(is_legal_duplicate_pair(&a, &b));
    }

    /// r01-W19: `pcr_field_range` must require `adaptation_field_length` to
    /// actually reach the 6-byte PCR field (>= 7: flags byte + PCR), not just
    /// check that the *packet* is long enough. Pre-fix, a packet whose
    /// adaptation field claims `PCR_flag` but declares `adaptation_field_length
    /// = 1` (no room for a PCR at all — this is itself a malformed/RFU-abusing
    /// packet, but must not be trusted) still had `pcr_field_range` return
    /// `Some(6..12)`, treating bytes 6..12 — which are unrelated payload
    /// bytes past the 1-byte adaptation field — as PCR-exempt. Two packets
    /// differing only in that payload region were then wrongly accepted as a
    /// legal §2.4.3.3 duplicate pair.
    #[test]
    fn pcr_exemption_requires_af_len_to_reach_the_pcr_field() {
        let pid = 0x0100;
        let afc_cc = 0x10 | ADAPTATION_FLAG;
        let mut a = [0xAAu8; 188];
        a[0] = 0x47;
        a[1] = ((pid >> 8) as u8) & 0x1F;
        a[2] = (pid & 0xFF) as u8;
        a[3] = afc_cc;
        a[4] = 1; // adaptation_field_length: flags byte only, no room for PCR
        a[5] = PCR_FLAG; // claims PCR_flag anyway
        let mut b = a;
        // Differ at byte 6 — real payload given af_len=1, NOT part of any
        // legitimate PCR field.
        b[6] = 0xBB;
        assert!(
            !is_legal_duplicate_pair(&a, &b),
            "byte 6 differs outside any PCR field af_len=1 can cover; must not be exempted"
        );

        // Sanity: the same layout WITH a conformant af_len (>= 7) still
        // treats that same byte range as the exempt PCR field.
        let mut c = a;
        c[4] = MIN_AF_LEN_WITH_PCR;
        let mut d = c;
        d[6] = 0xBB;
        assert!(
            is_legal_duplicate_pair(&c, &d),
            "byte 6 IS the PCR field once af_len actually covers it"
        );
    }

    #[test]
    fn pcr_only_difference_is_legal() {
        let a = packet_with_adaptation(0x0100, 0x10, [0, 0, 0, 0, 0, 0], None, None, 0xAB);
        let mut b = a;
        b[6..12].copy_from_slice(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC]);
        assert!(is_legal_duplicate_pair(&a, &b));

        // Bite-check: an unrelated byte difference elsewhere still fails —
        // proves the PCR exemption isn't accidentally exempting everything.
        let mut c = a;
        c[6..12].copy_from_slice(&[0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC]);
        c[20] ^= 0xFF;
        assert!(!is_legal_duplicate_pair(&a, &c));
    }

    #[test]
    fn payload_difference_is_illegal() {
        let a = payload_packet(0x0100, 0x10, 0xAB);
        let mut b = a;
        b[100] ^= 0xFF; // one payload byte differs
        assert!(!is_legal_duplicate_pair(&a, &b));
    }

    #[test]
    fn splice_countdown_difference_is_illegal() {
        let a = packet_with_adaptation(0x0100, 0x10, [0, 0, 0, 0, 0, 0], None, Some(5), 0xCD);
        let mut b = a;
        // Flip the splice_countdown byte only (offset 12, since no OPCR).
        b[12] ^= 0x01;
        assert!(!is_legal_duplicate_pair(&a, &b));
    }

    #[test]
    fn opcr_difference_is_illegal() {
        let a = packet_with_adaptation(
            0x0100,
            0x10,
            [0, 0, 0, 0, 0, 0],
            Some([1, 2, 3, 4, 5, 6]),
            None,
            0xCD,
        );
        let mut b = a;
        // OPCR occupies bytes 12..18; flip one of them.
        b[12] ^= 0x01;
        assert!(!is_legal_duplicate_pair(&a, &b));
    }

    #[test]
    fn wrong_adaptation_field_control_is_not_a_duplicate() {
        // Byte-identical, but AFC = '00' (reserved, no payload) on both —
        // not a "duplicate" under the spec's definition regardless of
        // byte-identity.
        let a = payload_packet(0x0100, 0x00, 0xAB);
        let b = a;
        assert!(!is_legal_duplicate_pair(&a, &b));

        // AFC = '10' (adaptation-only, no payload): also not a duplicate.
        let mut c = packet_with_adaptation(0x0100, 0x00, [0; 6], None, None, 0xAB);
        c[3] &= !PAYLOAD_FLAG; // clear payload bit -> afc = '10'
        let d = c;
        assert!(!is_legal_duplicate_pair(&c, &d));
    }

    #[test]
    fn third_consecutive_repeat_is_illegal() {
        let a = payload_packet(0x0100, 0x10, 0xAB);
        let b = a; // byte-identical

        // First transition: no prior duplicate consumed -> Legal.
        assert_eq!(check_duplicate(&a, &b, false), DuplicateVerdict::Legal);

        // Second transition (a third consecutive identical packet): the
        // caller already consumed the one legal repeat -> illegal.
        let c = a;
        assert_eq!(
            check_duplicate(&b, &c, true),
            DuplicateVerdict::IllegalThirdRepeat
        );
    }

    #[test]
    fn not_a_duplicate_reports_not_duplicate_regardless_of_state() {
        let a = payload_packet(0x0100, 0x10, 0xAB);
        let mut b = a;
        b[50] ^= 0xFF;
        assert_eq!(
            check_duplicate(&a, &b, false),
            DuplicateVerdict::NotDuplicate
        );
        assert_eq!(
            check_duplicate(&a, &b, true),
            DuplicateVerdict::NotDuplicate
        );
    }

    #[test]
    fn different_lengths_are_never_duplicates() {
        let a = [0u8; 188];
        let b = [0u8; 187];
        assert!(!is_legal_duplicate_pair(&a, &b));
    }

    #[test]
    fn too_short_for_afc_byte_is_never_a_duplicate() {
        let a = [0x47, 0x00, 0x10];
        let b = a;
        assert!(!is_legal_duplicate_pair(&a, &b));
    }
}