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:
+6
-1
@@ -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
@@ -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
@@ -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%
|
||||
|
||||
Reference in New Issue
Block a user