fix(libfreemkv): rc6 hardening pass — mux timeline/colour/PCR, demux panic sentinel, parser robustness + doc accuracy
Surgical fixes (each with a regression test that fails without the change): mux/mkv.rs, mux/demux_sink.rs: drive the clip-boundary timeline epoch off the resolved PRIMARY VIDEO track, not the literal stream index 0. An M2TS/PMT title can list an audio ES before video, so streams[0] may be audio; a non-video epoch driver ratchets the frontier and inflates the timeline. mkv cluster-opening falls back to track 0 for audio-only titles so they still open clusters. mux/codec/ac3.rs: correct ACMOD_CHANNELS — acmod=5 (3/1) is 4 channels, not 3 (was undercounting a 3/1 stream); fix the A/52 Table 5.8 doc. disc/mod.rs: HDMV coding_type 0x91 (Interactive Graphics / menus) no longer maps to PGS subtitle — it falls through to Unknown so the PMT/STN walker drops it instead of surfacing a bogus subtitle track. mux/videomap.rs + mux/mkv.rs: FVI colour now mirrors the MKV muxer's CICP precedence (measured CICP authoritative; HDR-driven PQ/HLG transfer override) via a shared cicp_for_video helper, so the two sinks can't disagree (HDR10 BT.2020 no longer emits SDR transfer 14). mux/mkvstream.rs: saturating_add on cluster_ts + rel_ts so an adversarial CLUSTER_TIMESTAMP near i64::MAX can't overflow/panic before the existing saturating_mul. mux/timeline.rs: tighten the tail-straggler clamp so a normal new-epoch non-video frame leading the sparse video frontier by >3s is not demoted into the previous clip's epoch. mux/m2ts_mux/mod.rs: re-stamp PCR per video TS packet (mid-PES), not only at PES boundaries, so a large UHD I-frame can't open a multi-second PCR gap; modular 33-bit PTS rebasing so a real 90 kHz clock wrap is not collapsed to PTS 0 (pre-base frames still floor to 0). io/byte_prefetcher.rs, sector/prefetched.rs: wrap the producer feed loop in catch_unwind and emit a typed error sentinel on panic, so a mid-stream producer panic is not read as a clean EOF at the demux boundary (which would silently truncate the mux). mux/codec/h264.rs: extend HIGH_PROFILES to the full ISO/IEC 14496-15 set that mandates the avcC chroma/bit-depth extension (adds 244 et al.). Doc/comment accuracy: css/mod.rs (50000 sectors, not scrambled-sectors), aacs/decrypt.rs (decrypt_unit already-clear path), ifo.rs (TT_SRPT at 0xC4), css/lfsr.rs (LFSR0 24-bit; TAB1-then-XOR cipher; real scramble-flag predicate), disc/read_error.rs (for_sweep does bounded transient retries). Skipped: keydb.rs SSRF guard (low/latent, no live caller) — a hard loopback block breaks an existing behavioral test that exercises the header-EOF path over a loopback server; a clean fix needs a resolver test seam beyond this surgical pass. The sibling keydb_fetch.rs comment fix is out of scope (freemkv crate).
This commit is contained in:
+52
-7
@@ -118,16 +118,21 @@ impl TimelineContinuity {
|
||||
// frontier and would force a forward-dated split cluster (breaking
|
||||
// cluster monotonicity). Such a straggler is recognised precisely: its
|
||||
// current-offset mapping lands more than a backstep PAST the frontier
|
||||
// AND its PREVIOUS-offset mapping lands at/below the frontier (i.e. it
|
||||
// belongs to the prior epoch). Remap it with the previous offset so
|
||||
// it lands at its true seam position. This is what distinguishes a
|
||||
// tail straggler from a frame that legitimately runs ahead of the
|
||||
// (base-video-only) frontier — a long audio-only tail, a sparse
|
||||
// subtitle, or an EL frame — which is left on the current offset.
|
||||
// AND its PREVIOUS-offset mapping lands in the seam TAIL — at/below the
|
||||
// frontier but no more than one backstep below it (i.e. it ended just
|
||||
// before the seam, in the prior epoch). The lower bound is essential:
|
||||
// a NORMAL new-epoch frame that merely leads the sparse (video-only)
|
||||
// frontier by >3s ALSO has `prev_mapped <= high` (its prev-offset
|
||||
// mapping lands ~a whole clip below the frontier), and clamping it
|
||||
// would demote it into the just-ended clip's epoch, mis-timing that
|
||||
// audio/subtitle by a whole clip. Requiring `prev_mapped` to sit
|
||||
// within a backstep below the frontier keeps the remap to genuine
|
||||
// tail stragglers; a long audio-only tail, a sparse subtitle, or an
|
||||
// EL frame that simply runs ahead is left on the current offset.
|
||||
if let Some(high) = self.high_ns {
|
||||
if mapped > high + DISCONTINUITY_BACKSTEP_NS {
|
||||
let prev_mapped = raw_pts_ns.saturating_add(self.prev_offset_ns);
|
||||
if prev_mapped <= high {
|
||||
if prev_mapped <= high && prev_mapped >= high - DISCONTINUITY_BACKSTEP_NS {
|
||||
return prev_mapped;
|
||||
}
|
||||
}
|
||||
@@ -418,4 +423,44 @@ mod tests {
|
||||
let normal = adj_other(&mut tc, S);
|
||||
assert_eq!(normal, S + 600 * S + DISCONTINUITY_GAP_NS);
|
||||
}
|
||||
|
||||
/// Regression for the over-eager straggler clamp: a NORMAL new-epoch
|
||||
/// non-video frame that leads the (sparse, video-only) frontier by MORE than
|
||||
/// one backstep must ride the CURRENT offset — it must NOT be demoted into
|
||||
/// the just-ended clip's epoch. Such a frame satisfies BOTH of the old
|
||||
/// discriminator's conditions (current-map > frontier+backstep AND
|
||||
/// prev-map <= frontier), so the old `prev_mapped <= high` test wrongly
|
||||
/// clamped it back ~a whole clip. The tightened lower bound
|
||||
/// (`prev_mapped >= high - backstep`) fixes it.
|
||||
#[test]
|
||||
fn normal_new_epoch_frame_leading_frontier_is_not_clamped() {
|
||||
let mut tc = TimelineContinuity::new();
|
||||
// Clip1 video rises to 600s, then clip2 resets to 0 → boundary.
|
||||
for i in 0..=600 {
|
||||
adj_video(&mut tc, i * S);
|
||||
}
|
||||
let frontier = tc.high_ns.unwrap();
|
||||
assert_eq!(frontier, 600 * S);
|
||||
let c2 = adj_video(&mut tc, 0);
|
||||
assert_eq!(c2, 600 * S + DISCONTINUITY_GAP_NS);
|
||||
|
||||
// A NORMAL clip-2 audio frame at raw ~5s. Current-offset mapping is
|
||||
// ~605s, which IS more than a backstep (3s) past the 600s frontier — but
|
||||
// its previous-offset mapping (~5s) lands ~595s BELOW the frontier, far
|
||||
// outside the seam tail. It is a legitimate new-epoch frame, NOT a tail
|
||||
// straggler, and must ride the current offset.
|
||||
let raw = 5 * S;
|
||||
let out = adj_other(&mut tc, raw);
|
||||
assert_eq!(
|
||||
out,
|
||||
raw + 600 * S + DISCONTINUITY_GAP_NS,
|
||||
"a normal new-epoch frame leading the frontier by >3s must ride the \
|
||||
current offset, not be clamped back into the previous clip"
|
||||
);
|
||||
// And it must NOT have been demoted near the previous clip's tail (~5s).
|
||||
assert!(
|
||||
out > frontier,
|
||||
"frame must stay in the new epoch (> frontier), got {out}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user