From 65c68353637f892fba1ae657c1d03a4ac13e149e Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:09:28 -0700 Subject: [PATCH] disc: add progress_snapshot_from_mapfile one-shot + locate_ranges test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The library now offers a one-shot PassProgress snapshot built from a mapfile on disk + title, so a client gets the fully-rendered drilldown without parsing the mapfile itself — used for autorip's pass-boundary and mux-entry paints (no live callback yet at those points). Adds a locate_ranges test covering in-feature vs out-of-feature at-risk time (ported from autorip's removed from_map tests). --- src/disc/mod.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/disc/mod.rs b/src/disc/mod.rs index e045f27..9809ee5 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -685,6 +685,44 @@ pub fn locate_ranges(raw: &[(u64, u64)], title: &DiscTitle) -> crate::progress:: } } +/// One-shot progress snapshot built from a mapfile on disk plus the title. The +/// library reads + parses the mapfile HERE so a client (autorip) gets a +/// fully-rendered [`crate::progress::PassProgress`] without ever touching +/// mapfile internals — used for the pass-boundary paint (before the live +/// callback stream begins) and the terminal done-card verdict. Returns `None` +/// if the mapfile can't be read. `work_done`/`work_total` are `0`: this is a +/// point-in-time snapshot, not a per-pass progress tick. +pub fn progress_snapshot_from_mapfile( + mapfile_path: &std::path::Path, + title: Option<&DiscTitle>, + kind: crate::progress::PassKind, + bytes_total_disc: u64, +) -> Option { + use mapfile::SectorStatus::{NonScraped, NonTrimmed, Unreadable}; + let map = mapfile::Mapfile::load(mapfile_path).ok()?; + let stats = map.stats(); + // MAYBE set = not-yet-good (NonTrimmed/NonScraped/Unreadable), excluding + // NonTried (the unread remainder) — same set the live patch emitter uses. + let maybe = map.ranges_with(&[NonTrimmed, NonScraped, Unreadable]); + let located = title.map(|t| locate_ranges(&maybe, t)).unwrap_or_default(); + let main_bad = title.map(|t| bytes_bad_in_title(t, &maybe)).unwrap_or(0); + Some(crate::progress::PassProgress { + kind, + work_done: 0, + work_total: 0, + bytes_good_total: stats.bytes_good, + bytes_unreadable_total: stats.bytes_unreadable, + bytes_pending_total: stats.bytes_pending, + bytes_retryable_total: stats.bytes_retryable, + bytes_total_disc, + disc_duration_secs: title.map(|t| t.duration_secs), + bytes_bad_in_main_title: main_bad, + main_title_duration_secs: title.map(|t| t.duration_secs), + main_title_size_bytes: title.map(|t| t.size_bytes), + located, + }) +} + // ─── Display helpers ──────────────────────────────────────────────────────── impl Codec { @@ -4181,6 +4219,40 @@ mod tests { } } + #[test] + fn locate_ranges_at_risk_in_vs_out_of_feature() { + // The honest-"Maybe" behaviour the rendered drilldown must preserve: + // in-feature damage counts as movie time at risk; out-of-feature damage + // is still located but reads 0:00. (Ported from autorip's old + // RipProgress::from_map tests when that logic moved into the library.) + // bps = size_bytes / duration_secs = 4096 B/s → 4096 B == 1000 ms. + let mut title = title_with_video(Codec::Hevc, Resolution::R2160p); + title.duration_secs = 100.0; + title.size_bytes = 409_600; + // Feature extent = sectors [10,110) → bytes [20480, 225280). + title.extents = vec![Extent { + start_lba: 10, + sector_count: 100, + }]; + + // In-feature pending range: 4096 B == 1000 ms of movie at risk. + let in_feat = locate_ranges(&[(40_960, 4096)], &title); + assert_eq!(in_feat.num_ranges, 1); + assert!( + (in_feat.main_at_risk_ms - 1000.0).abs() < 1.0, + "in-feature damage must count as at-risk movie time, got {}", + in_feat.main_at_risk_ms + ); + + // Out-of-feature range: located, but zero movie time at risk. + let out_feat = locate_ranges(&[(2_000_000, 100_000)], &title); + assert_eq!(out_feat.num_ranges, 1, "still a located range"); + assert_eq!( + out_feat.main_at_risk_ms, 0.0, + "out-of-feature damage must not read as movie loss" + ); + } + /// Build a DiscTitle with full control over the fields the title /// sorter cares about. Used by the canonical-title-order tests. fn title_with(