container-probe 0.1.0

Robust media container-format detection — MPEG-2 TS (188/192/204/208 stride + phase), ISOBMFF, Matroska/WebM, MPEG-PS, FLV, MXF, WAV, Ogg, ASF, and elementary streams.
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
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
//! Real-fixture and robustness tests for [`container_probe`] — Work-package 1:
//! the MPEG-2 TS prober ([`container_probe::ts`]).
//!
//! Paths are relative to the workspace root; from a test we build them with
//! [`env!("CARGO_MANIFEST_DIR")`](./env) joined to `../<path>` because fixtures
//! are committed at the workspace's `fixtures/` tree, outside this crate.
//!
//! The stride/phase values asserted here were measured from the real files and
//! are correct; if the code disagrees with them the code is wrong.

use container_probe::{Confidence, Detail, Format, Probe};
use std::fs;

/// Join a workspace-relative fixture path to an absolute path from this crate.
fn fixture(rel: &str) -> Vec<u8> {
    fs::read(format!("{}/../{}", env!("CARGO_MANIFEST_DIR"), rel))
        .unwrap_or_else(|e| panic!("failed to read fixture {rel}: {e}"))
}

/// `LATTICE_STRONG` confidence — reused by name so assertions read clearly.
const LATTICE_STRONG: Confidence = Confidence::LATTICE_STRONG;

/// M2TS: 192-byte packets (a 4-byte `TP_extra_header` per 188-byte TS packet),
/// first sync at byte offset 4.
///
/// # Mutation proof
///
/// Removing the 192 stride from `TS_STRIDES` (and dropping the array length to
/// 3) breaks this test. Re-measured after the confidence/coverage fix and the
/// round-3 `Insufficient`-contract fix, the file then probes:
/// ```
/// probe mismatch: Insufficient { need_at_least: 83098 }
/// ```
/// because no remaining stride's lattice reaches the weak threshold. (Earlier
/// revisions surfaced first `Unknown`, then `Insufficient { need_at_least: 55484 }`,
/// as the no-candidate branch changed.) The stride was restored.
#[test]
fn m2ts_192_stride() {
    let p = probe_fixture("fixtures/container-probe/m2ts_192.m2ts");
    assert_identical_ts(p, 192, 4, LATTICE_STRONG);
}

/// The mid-packet phase test.
///
/// # Mutation proof
///
/// This test bites: **deleting the phase loop** — so that only `phase == 0` is
/// ever probed for each stride — makes this file fail to identify as TS,
/// because the capture begins 111 bytes into a packet and there is no `0x47`
/// at offset 0. Observed failure (re-measured after the confidence/coverage
/// fix and the round-3 `Insufficient`-contract fix; with the loop collapsed to
/// `for phase in 0..0`):
/// ```
/// probe mismatch: Insufficient { need_at_least: 66686 }
/// ```
/// (Earlier this mutation surfaced first as `Unknown`, then as
/// `Insufficient { need_at_least: 65724 }`, before the no-candidate branch
/// changed.) The loop was restored.
#[test]
fn ts_midpacket_phase() {
    let p = probe_fixture("fixtures/container-probe/ts_midpacket_phase.ts");
    assert_identical_ts(p, 188, 111, LATTICE_STRONG);
}

#[test]
fn ts_204_stride_synthetic() {
    let p = probe_fixture("fixtures/container-probe/ts_204_stride_SYNTHETIC.ts");
    assert_identical_ts(p, 204, 0, LATTICE_STRONG);
}

#[test]
fn h264_aac_188_stride() {
    let p = probe_fixture("fixtures/ts/h264_aac.ts");
    assert_identical_ts(p, 188, 0, LATTICE_STRONG);
}

/// Large real DVB captures (multimegabyte) the suite previously did not cover —
/// each must resolve to a 188-byte-stride, phase-0 lattice at `LATTICE_STRONG`.
#[test]
fn france2_capture() {
    let p = probe_fixture("fixtures/ts/france2.ts");
    assert_identical_ts(p, 188, 0, LATTICE_STRONG);
}

#[test]
fn gulli_opengop_capture() {
    let p = probe_fixture("fixtures/ts/gulli-opengop.ts");
    assert_identical_ts(p, 188, 0, LATTICE_STRONG);
}

#[test]
fn tnt5w_capture() {
    let p = probe_fixture("fixtures/dvb-si/tnt-5w-12732v-isi6-10s.ts");
    assert_identical_ts(p, 188, 0, LATTICE_STRONG);
}

/// Assert a file is identified as `format` (WP2+ structural probers). Keeps the
/// WP1 "not TS" contract: these files are definitive non-TS containers.
fn assert_format(rel: &str, format: Format) {
    let p = probe_fixture(rel);
    assert!(
        matches!(p, Probe::Identified { format: f, .. } if f == format),
        "{rel} must be Identified {format:?}, got {p:?}"
    );
}

#[test]
fn mp4_is_not_ts() {
    assert_format("fixtures/mp4/h264_high.mp4", Format::Isobmff);
}

#[test]
fn wav_is_not_ts() {
    assert_format("fixtures/container-probe/pcm_s16le.wav", Format::Wav);
}

#[test]
fn ogg_is_not_ts() {
    assert_format("fixtures/container-probe/opus.ogg", Format::Ogg);
}

#[test]
fn mkv_is_not_ts() {
    assert_format("fixtures/mkv/h264_aac.mkv", Format::Matroska);
}

#[test]
fn flv_is_not_ts() {
    assert_format("fixtures/flv/av.flv", Format::Flv);
}

#[test]
fn asf_is_not_ts() {
    assert_format("fixtures/container-probe/video.asf", Format::Asf);
}

/// Regression guard for the CENC **TS-misidentification** false positive
/// (WP1): the encrypted payload aligned 3-byte `0x47` runs on one lane.
///
/// This CENC-encrypted MP4 (high-entropy encrypted payload) previously probed
/// to a confident `MpegTs` at `Ts { stride: 208, phase: 142 }`, conf 96
/// (`LATTICE_WEAK`): across the 792 lanes, three consecutive `0x47` bytes
/// aligned on one lane purely by chance. A confident wrong answer is the worst
/// outcome a probe can produce. The fix required a candidate lane to *cover* at
/// least `TS_MIN_COVERAGE_PCT` of its positions with sync bytes — a real TS
/// stream syncs at ~100% of positions, random noise at ~2.5%.
///
/// WP2 adds the ISOBMFF prober, so the whole file now correctly identifies as
/// `Isobmff` (leading `ftyp`, major brand `isom`).
///
/// **This test alone no longer guards the coverage rule** — see
/// [`cenc_payload_without_its_boxes_is_not_ts`], which does. Once ISOBMFF is
/// registered, this file matches it at `STRUCTURAL` (160) while the spurious TS
/// lattice scores only `LATTICE_WEAK` (96); 160 beats 96 by far more than
/// `TIE_THRESHOLD`, so the verdict stays `Isobmff` even with the coverage gate
/// deleted. Verified: with `TS_MIN_COVERAGE_PCT` set to `0` and a forced
/// rebuild, the whole suite still passed. A guard that cannot fail is not a
/// guard.
#[test]
fn cenc_mp4_is_isobmff() {
    assert_format("fixtures/mp4/cenc.mp4", Format::Isobmff);
}

/// The real guard for `TS_MIN_COVERAGE_PCT`, isolating the TS prober from
/// ISOBMFF's shadow.
///
/// `fixtures/mp4/cenc.mp4` carries a high-entropy CENC-encrypted payload. Across
/// the 792 lanes (188+192+204+208 phases) three consecutive `0x47` bytes align
/// on one lane purely by chance — measured at stride 208, phase 141, with a
/// longest run of 3 and just 4 sync bytes over 117 lane positions (3% coverage).
/// Run length alone called that a match; a confident wrong answer is the worst
/// outcome a probe can produce.
///
/// Skipping the first byte removes the valid box header at offset 0, so the
/// ISOBMFF prober cannot match and cannot mask the TS prober's verdict. What
/// remains is exactly the encrypted noise the coverage rule exists to reject.
///
/// # Mutation proof
///
/// Re-measured against this test, with a **forced rebuild** (`touch` on the
/// source — a restored file with an older mtime leaves cargo serving a stale
/// binary, which briefly produced a false result during this investigation).
/// Setting `TS_MIN_COVERAGE_PCT` to `0`:
/// ```
/// assertion `left == right` failed
///   left: Identified { format: MpegTs, confidence: Confidence(96),
///          detail: Ts { stride: 208, phase: 141 } }
///   right: Unknown
/// ```
/// Restored to `50`, this slice is `Unknown`.
#[test]
fn cenc_payload_without_its_boxes_is_not_ts() {
    let data = fixture("fixtures/mp4/cenc.mp4");
    let p = probe(&data[1..]);
    assert_eq!(
        p,
        Probe::Unknown,
        "encrypted payload with no box header must not match any prober, got {p:?}"
    );
}

#[test]
fn av1_mp4_is_not_ts() {
    assert_format("fixtures/mp4/av1.mp4", Format::Isobmff);
}

#[test]
fn vp9_opus_mkv_is_not_ts() {
    assert_format("fixtures/mkv/vp9_opus.mkv", Format::Matroska);
}

#[test]
fn vp8_opus_webm_is_not_ts() {
    assert_format("fixtures/webm/vp8_opus.webm", Format::WebM);
}

#[test]
fn ps_is_not_ts() {
    assert_format("fixtures/ps/h264_ac3.ps", Format::MpegPs);
}

#[test]
fn mxf_is_not_ts() {
    assert_format("fixtures/mxf/op1a_mpeg2_pcm.mxf", Format::Mxf);
}

#[test]
fn adts_aac_is_not_ts() {
    assert_format("fixtures/container-probe/aac.adts", Format::AdtsAac);
}

#[test]
fn mp3_is_not_ts() {
    assert_format("fixtures/container-probe/audio.mp3", Format::Mp3);
}

#[test]
fn annexb_is_not_ts() {
    assert_format("fixtures/container-probe/h264.annexb", Format::AnnexB);
}

// ---------------------------------------------------------------------------
// Negative / robustness cases — each must be `Unknown` or `Insufficient`,
// never a confident wrong answer, and must never panic.
// ---------------------------------------------------------------------------

/// Nothing has been ruled out yet, so the honest answer is "read more" — the
/// 4 bytes of the shortest magic any prober needs.
#[test]
fn empty_slice() {
    match probe(&[]) {
        Probe::Insufficient { need_at_least, .. } => assert_eq!(need_at_least, 4),
        other => panic!("an empty slice must be Insufficient {{ 4 }}, got {other:?}"),
    }
}

/// One arbitrary byte still cannot rule out a 4-byte magic.
#[test]
fn single_byte() {
    match probe(&[0x42]) {
        Probe::Insufficient { need_at_least, .. } => assert_eq!(need_at_least, 4),
        other => panic!("a single byte must be Insufficient {{ 4 }}, got {other:?}"),
    }
}

/// Eight zero bytes match no magic but are too short to rule out a structure
/// beginning with a zero-valued box size, so the answer asks for ground not yet
/// examined.
///
/// The figure is 13, not 9: no prober can name a structural need here, so the
/// answer comes from the geometric floor (`limit + limit/2 + 1`). An
/// arithmetic `limit + 1` also satisfies "more than examined" and is what this
/// asserted before — but it converges in O(n) reads, which terminates and
/// crawls. The exact number matters less than which growth class it belongs to.
#[test]
fn eight_zero_bytes() {
    match probe(&[0u8; 8]) {
        Probe::Insufficient { need_at_least, .. } => assert_eq!(need_at_least, 13),
        other => panic!("eight zero bytes must be Insufficient {{ 13 }}, got {other:?}"),
    }
}

/// 4096 bytes of `0xFF`: no sync byte, no magic, and far more than any prober
/// needs to decide. Every format is ruled out, so this is `Unknown` ("stop") —
/// exactly, not "Unknown or Insufficient". The disjunction this replaces
/// accepted whichever the code happened to return, while the doc comment above
/// it already asserted `Unknown`; a guard that agrees with any answer cannot
/// notice the two diverging.
#[test]
fn ff_bytes() {
    let p = probe(&[0xFFu8; 4096]);
    assert!(
        matches!(p, Probe::Unknown),
        "4096 bytes of 0xFF rule out every format, so this must be Unknown, got {p:?}"
    );
}

/// 70,000 zero bytes with a single `0x47` at offset 12,345. The lone sync byte
/// — the ASCII "G" — seeds a run of exactly 1 on every stride lane, but the
/// region is far longer than any lattice needs to prove itself, so nothing
/// reaching the weak threshold means `Unknown`, never an endless
/// `Insufficient`/"read more".
#[test]
fn zeros_with_single_sync_byte() {
    let mut data = vec![0u8; 70_000];
    data[12_345] = 0x47;
    let p = probe(&data);
    assert_eq!(p, Probe::Unknown, "got {p:?}");
}

/// A short but complete-looking transport stream: the first 600 bytes of
/// `h264_aac.ts` is 4 whole 188-byte packets, every lattice position a sync
/// byte. That is a **match** — weakly, since it is below the 8 confirmations
/// `LATTICE_STRONG` needs — never `Insufficient`.
///
/// This is the regression guard for a real defect. An earlier revision
/// downgraded any qualifying lane to `Insufficient` when the buffer was too
/// short to reach `LATTICE_STRONG`, on the reasoning that a truncated sample is
/// unproven. A sweep of all 145 media files in the repo showed what that
/// actually did: eleven real and *complete* TS files of 188 B - 1.1 KB
/// (`fixtures/ts/scte35-*.ts`, `fixtures/ts/pts-*.ts`,
/// `fixtures/mpeg-ts/af-*.ts`) answered `Insufficient { need_at_least: 1504 }`
/// — telling a caller to read past the end of a file it had fully read.
///
/// MUTATION VERIFIED: restoring the `could_reach_strong` downgrade turns this
/// red with `Insufficient { need_at_least: 1504 }`.
#[test]
fn short_but_complete_ts_is_a_weak_match_not_insufficient() {
    let data = fixture("fixtures/ts/h264_aac.ts");
    let p = probe(&data[..600]);
    assert_identical_ts(p, 188, 0, Confidence::LATTICE_WEAK);
}

/// A single 188-byte packet is genuinely too little to conclude from: one sync
/// byte at offset 0 is one confirmation, below `TS_CONFIRM_FOR_WEAK`. Here
/// `Insufficient` IS correct — there is a coherent start and more bytes really
/// would settle it. This is the boundary case that proves the fix above did not
/// simply delete the `Insufficient` path.
#[test]
fn a_single_ts_packet_is_insufficient() {
    let data = fixture("fixtures/ts/h264_aac.ts");
    match probe(&data[..188]) {
        Probe::Insufficient { need_at_least, .. } => {
            assert!(
                need_at_least > 188,
                "need_at_least {need_at_least} must exceed the 188 supplied"
            );
        }
        other => panic!("a single TS packet must be Insufficient, got {other:?}"),
    }
}

/// Small but complete TS fixtures carrying at least `TS_CONFIRM_FOR_WEAK`
/// packets identify, rather than asking for bytes that do not exist.
///
/// These are the files a sweep of every media file in the repo caught reporting
/// `Insufficient { need_at_least: 1504 }` — each is a whole file on disk of
/// 752-1128 bytes (4-6 whole packets, every lattice position a sync byte), so
/// the caller can never supply more. See
/// [`short_but_complete_ts_is_a_weak_match_not_insufficient`] for the defect.
///
/// The threshold is packet count, not file completeness: a fixture below
/// `TS_CONFIRM_FOR_WEAK` packets is covered by
/// [`tiny_ts_fixtures_below_the_weak_threshold_are_insufficient`] instead.
#[test]
fn small_complete_ts_fixtures_all_identify() {
    for path in [
        "fixtures/mpeg-ts/af-transport-private-data.ts", // 752 B — 4 packets
        "fixtures/ts/pcr-wrap.ts",                       // 940 B — 5 packets
        "fixtures/ts/pts-backward.ts",                   // 940 B — 5 packets
        "fixtures/ts/pts-wrap.ts",                       // 940 B — 5 packets
        "fixtures/ts/scte35-pcr.ts",                     // 1128 B — 6 packets
    ] {
        let data = fixture(path);
        let p = probe(&data);
        match &p {
            Probe::Identified {
                format,
                confidence,
                detail: Detail::Ts { stride, phase, .. },
                ..
            } => {
                assert_eq!(*format, Format::MpegTs, "{path}");
                assert_eq!(*confidence, Confidence::LATTICE_WEAK, "{path}");
                assert_eq!(*stride, 188, "{path}");
                assert_eq!(*phase, 0, "{path}");
            }
            other => panic!("{path} ({} bytes) must identify, got {other:?}", data.len()),
        }
    }
}

/// A complete file can still be too small to conclude from. These fixtures hold
/// one or two whole packets — below `TS_CONFIRM_FOR_WEAK` contiguous
/// confirmations — so `Insufficient` is the honest verdict even though no more
/// bytes exist on disk.
///
/// The probe takes a slice and is given no end-of-file signal, so it cannot know
/// the caller has nothing further; reporting a confident `MpegTs` from a single
/// `0x47` at offset 0 would be a guess. A caller that knows it is at EOF treats
/// `Insufficient` as "undecidable from this file", which is exactly right for
/// 188 bytes.
#[test]
fn tiny_ts_fixtures_below_the_weak_threshold_are_insufficient() {
    for path in [
        "fixtures/ts/emsg-pid4.ts",            // 188 B — 1 packet
        "fixtures/ts/scte35-balanced.ts",      // 188 B — 1 packet
        "fixtures/ts/scte35-real.ts",          // 188 B — 1 packet
        "fixtures/ts/scte35-unbalanced.ts",    // 188 B — 1 packet
        "fixtures/mpeg-ts/af-pcr-stuffing.ts", // 376 B — 2 packets
    ] {
        let data = fixture(path);
        match probe(&data) {
            Probe::Insufficient { need_at_least, .. } => {
                assert!(need_at_least > data.len(), "{path}");
            }
            other => panic!(
                "{path} ({} bytes) is below the weak threshold and must be \
                 Insufficient, got {other:?}",
                data.len()
            ),
        }
    }
}

/// 4096 bytes of `0x47` — a pathological TS-shaped buffer with no real
/// structure.
///
/// A naive lattice gives *every* stride lane a run of ~20 syncs (the whole
/// buffer is `0x47`), which would score `LATTICE_STRONG`. That is a false
/// positive. Our prober rejects a region that is *uniformly* the sync byte:
/// `0x47` is "G", and a continuous run with no packet content to fill the
/// lanes is not a real transport stream (a genuine 188-byte packet has exactly
/// one sync byte and 187 other bytes). The uniform-buffer guard therefore
/// returns `Unknown` here — defensible because the all-`0x47` file carries no
/// evidence of packet structure, only of the sync character repeating.
#[test]
fn all_sync_bytes() {
    let p = probe(&[0x47u8; 4096]);
    assert!(
        matches!(p, Probe::Unknown),
        "got {p:?} — a full all-sync input must be ruled out, not flagged as TS"
    );
}

/// Budget test. The 188-stride fixture probed with a 512-byte budget reads no
/// further than 512 bytes (the harness caps the region at `min(len, budget)`),
/// so a full strong lattice (8 confirmations at 188 bytes each = 1504+ bytes)
/// cannot form. The best lane reaches exactly 3 syncs -> a `LATTICE_WEAK`
/// verdict, a genuinely *weaker* conclusion than the unbounded `LATTICE_STRONG`
/// in `h264_aac_188_stride`. That is the saner of the two allowed outcomes and
/// shows the budget is actually clamped.
#[test]
fn budget_caps_the_read() {
    let data = fixture("fixtures/ts/h264_aac.ts");
    let p = probe_with_budget(&data, 512);
    match p {
        Probe::Identified {
            format,
            confidence,
            detail,
            ..
        } => {
            assert_eq!(format, Format::MpegTs);
            assert_eq!(confidence, Confidence::LATTICE_WEAK);
            // `Detail::Ts` is `#[non_exhaustive]`, so it cannot be *constructed*
            // here; match it structurally and assert each field.
            match detail {
                Detail::Ts { stride, phase, .. } => {
                    assert_eq!(stride, 188);
                    assert_eq!(phase, 0);
                }
                other => panic!("expected Ts detail, got {other:?}"),
            }
        }
        Probe::Insufficient { need_at_least, .. } => {
            // Also acceptable; the buffer was too short to prove the lattice.
            assert!(need_at_least > 512);
        }
        other => panic!("budget probe returned {other:?}; expected a weak verdict or Insufficient"),
    }
}

/// Finding 10: `need_at_least` must be sized by the budget the prober actually
/// read (the capped region), not the caller's whole buffer. Probing a 10 MB TS
/// with a 64-byte budget must answer "supply a few hundred bytes more", never
/// "supply 10 MB more" — the budget was the constraint, not a short file.
#[test]
fn budget_sizes_the_insufficient_hint() {
    let data = fixture("fixtures/ts/h264_aac.ts");
    let p = probe_with_budget(&data, 64);
    match p {
        Probe::Insufficient { need_at_least, .. } => {
            // A genuine lower bound (a 188-byte packet × the strong threshold),
            // but never scaled to the ignored full buffer length.
            assert!(
                need_at_least > 64,
                "need_at_least {need_at_least} must exceed the 64-byte budget"
            );
            assert!(
                need_at_least < data.len() + 188,
                "need_at_least {need_at_least} must not be sized by the full {} byte buffer",
                data.len()
            );
        }
        other => panic!("a 64-byte budget on a TS must be Insufficient, got {other:?}"),
    }
}

/// Run [`container_probe::ts::probe`]-level assertion helper: unwrap an
/// `Identified` TS result and check the format, confidence, stride and phase.
///
/// The `Detail::Ts` variant is `#[non_exhaustive]` (so adding a field is not
/// breaking), which means a downstream crate cannot *construct* it with a struct
/// expression to compare against. This helper matches it structurally instead
/// and asserts each field individually, keeping the exactness the old
/// `assert_eq!(p, Probe::Identified { .. Detail::Ts { .. } })` had.
fn assert_identical_ts(p: Probe, stride: u16, phase: u16, confidence: Confidence) {
    match p {
        Probe::Identified {
            format: Format::MpegTs,
            confidence: c,
            detail:
                Detail::Ts {
                    stride: s,
                    phase: ph,
                    ..
                },
            ..
        } => {
            assert_eq!(c, confidence, "probe mismatch: confidence");
            assert_eq!(s, stride, "probe mismatch: stride");
            assert_eq!(ph, phase, "probe mismatch: phase");
        }
        other => panic!("probe mismatch: {other:?}"),
    }
}

fn probe(data: &[u8]) -> Probe {
    container_probe::probe(data)
}

fn probe_with_budget(data: &[u8], budget: usize) -> Probe {
    container_probe::probe_with_budget(data, budget)
}

/// Load a fixture and probe it.
fn probe_fixture(rel: &str) -> Probe {
    probe(&fixture(rel))
}