fix: assorted correctness fixes and dead-code cleanup

- aacs/resolve: a media-keys-only provider missing the VID classifies as
  VidUnavailable, not NoMaterial (an MK derives the VUK once the VID
  arrives).
- disc/bluray: mark a clip seen only after its .clpi parses, so a
  transient parse failure on the first PlayItem cannot suppress the
  clip's extents for a later PlayItem referencing it that succeeds.
- disc/patch: log rather than swallow mapfile record/flush failures on a
  reverify downgrade, so a failed persist cannot silently mismark a bad
  unit good on resume.
- mux/ts: flag a discontinuity when a partial PES is dropped, matching
  the other partial-drop paths.
- mux/demux_thread: the no-demuxer branch forwards an empty batch for
  early consumer-disconnect detection instead of reading the whole disc.
- io/pipeline: correct the send-timing log (as_secs_f64, not as_micros
  printed as ms).
- aacs/derive, aacs/variant, disc/read_error, keysource: comment/doc
  accuracy. sector/prefetched, udf: remove dead fields/functions.
- mux/disc: assert unit-aligned read counts in the test.
This commit is contained in:
Matthew Jackson
2026-07-08 14:44:15 -07:00
parent 67aba17173
commit 0d587d1154
13 changed files with 95 additions and 114 deletions
+6 -1
View File
@@ -85,13 +85,18 @@ impl Disc {
for play_item in &parsed.play_items {
let clip_dur = play_item.out_time.saturating_sub(play_item.in_time) as f64 / 45000.0;
let mut pkt_count: u32 = 0;
let first_ref = seen_clips.insert(play_item.clip_id.clone());
let clpi_path = format!("/BDMV/CLIPINF/{}.clpi", play_item.clip_id);
if let Ok(clpi_data) = udf_fs.read_file(reader, &clpi_path) {
if let Ok(clip_info) = clpi::parse(&clpi_data) {
pkt_count = clip_info.source_packet_count;
// Mark the clip seen ONLY after its .clpi parses — a transient
// read/parse failure on the first PlayItem referencing a clip
// must not permanently suppress its extents/size for a later
// PlayItem referencing the same clip that succeeds.
let first_ref = seen_clips.insert(play_item.clip_id.clone());
// Only fetch/push the physical extents and add to the
// total size the first time this clip_id is seen.
if first_ref {
+19 -13
View File
@@ -473,9 +473,7 @@ pub(super) fn recovery_read<R: SectorSource + ?Sized>(
/// gets recorded NonTrimmed. Pure data structure — no I/O — so each phase
/// helper is unit-testable by asserting the residual `SubRanges`.
///
/// Foundation for the phased `recover_section` orchestrator; not yet wired
/// into the live loop (see the deferral note in the #50 work).
#[cfg_attr(not(test), allow(dead_code))]
/// The residue tracker used by the phased `recover_section` orchestrator.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(super) struct SubRanges {
/// (pos, len) pairs, sorted by pos, non-overlapping, all non-zero len.
@@ -1461,13 +1459,22 @@ impl Disc {
if !bad.is_empty() {
let n: usize = bad.len();
for (lba, cnt) in bad {
let _ = m.record(
if let Err(e) = m.record(
lba as u64 * 2048,
cnt as u64 * 2048,
mapfile::SectorStatus::NonTrimmed,
) {
tracing::warn!(
lba,
"reverify downgrade: mapfile record failed ({e}) — unit may stay mismarked as good"
);
}
}
if let Err(e) = m.flush() {
tracing::warn!(
"reverify downgrade: mapfile flush failed ({e}) — downgrade not persisted; a resume could mismark it good"
);
}
let _ = m.flush();
// The re-verify ran AFTER `pipe.finish()` snapshotted
// `summary.stats`, so those stats still count the just-
// downgraded units as good. Refresh from the mapfile so
@@ -1547,14 +1554,13 @@ mod tests {
assert!(on("true"));
}
/// Transport failure (status=0xFF, USB-bridge crash) must be recognised by
/// the gate `handle_read_failure` now checks FIRST, so it aborts the pass
/// (wedged_exit + BreakOuter) instead of treating the bridge crash as an
/// ordinary bad sector and hammering the crashed device for up to the
/// per-range watchdog budget. `handle_read_failure` is not unit-testable in
/// isolation, so this guards the classification predicate the production
/// early-return keys off, and the contrast that an ordinary read error is
/// NOT misclassified as a transport failure.
/// Transport failure (status=0xFF, USB-bridge crash) must be recognised and
/// abort the pass, rather than being treated as an ordinary bad sector and
/// hammering the crashed device for up to the per-range watchdog budget. The
/// transport-failure classification predicate is not unit-testable in
/// isolation, so this guards the predicate the production early-return keys
/// off, and the contrast that an ordinary read error is NOT misclassified as
/// a transport failure.
#[test]
fn transport_failure_is_recognised_for_patch_abort() {
use crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE;
+6 -18
View File
@@ -196,22 +196,10 @@ impl ReadCtx {
/// threshold is loose so we don't bail too early on a range that
/// has scattered good sectors mixed in.
///
/// `damage_threshold_pct = 6` mirrors `disc/patch.rs`'s
/// `PASSN_DAMAGE_THRESHOLD_PCT`. Pass N triggers the damage-skip
/// at half the density Pass 1 uses (Pass 1 = 12%) because the
/// patch loop's whole job is to chip away at bad ranges — being
/// more eager to skip clustered bad sectors converges faster on
/// the recoverable good sectors inside a range. The patch-side
/// `compute_damage_skip` reads its threshold directly from
/// `PASSN_DAMAGE_THRESHOLD_PCT`, which is an alias for this crate's
/// `PATCH_DAMAGE_THRESHOLD_PCT`, so the two are always in sync.
/// The patch loop's damage-skip is not yet unified with `handle_read_error`'s
/// jump path. (v0.20.8 unification attempt found the unification
/// itself blocked on the size-aware `range_remaining/4` cap that
/// lives in `compute_damage_skip` but not in
/// `handle_read_error::JumpAhead` — see
/// `tests/passn_handler_ab.rs` for the A/B fixture that pins
/// the divergence point.)
/// `damage_threshold_pct = 6` is looser than Pass 1 (12%): Pass N triggers
/// the damage-skip at half Pass 1 density because the patch loop exists to chip
/// away at bad ranges, so being more eager to skip clustered bad sectors
/// converges faster on the recoverable good sectors inside a range.
pub fn for_patch(batch: u16) -> Self {
Self {
batch,
@@ -443,8 +431,8 @@ const WEDGE_ABORT_THRESHOLD: u64 = 16;
const WEDGE_PASS_N_SKIP_SECTORS: u64 = 64;
/// Single source of truth for the Pass-N damage-window threshold.
/// Both [`ReadCtx::for_patch`] and `disc::patch::compute_damage_skip`
/// reference this constant so the two damage-skip paths cannot drift.
/// [`ReadCtx::for_patch`] reads this constant for the Pass-N damage-skip
/// threshold.
///
/// 6% means: with a 16-entry sliding window, the damage-skip fires
/// once 1 out of 16 recent reads has failed. Pass 1 uses a 12%