v0.13.24 — MapStats: split bytes_pending into nontried / retryable

bytes_pending was an opaque aggregate of NonTried + NonTrimmed +
NonScraped. UIs that wanted a "will retry in Pass 2-N" bucket were
stuck showing the entire unread disc as Maybe at pct=0.

Adds two granular fields to MapStats:

  bytes_nontried   — Pass 1 hasn't read these yet
  bytes_retryable  — NonTrimmed + NonScraped, Pass 2-N will retry

bytes_pending stays for back-compat (= bytes_nontried + bytes_retryable).

Also picks up the cargo fmt --check lint that's been red on main CI
since v0.13.18 (rustfmt fold differences on a few long format-string
layouts; functional no-op).
This commit is contained in:
MattJackson
2026-04-26 19:28:35 -07:00
parent 2cd4fbead7
commit ae76aaf0fa
9 changed files with 74 additions and 42 deletions
+23 -2
View File
@@ -69,12 +69,28 @@ pub struct MapEntry {
}
/// Summary statistics over all entries.
///
/// `bytes_pending` aggregates `NonTried + NonTrimmed + NonScraped` for
/// back-compat. `bytes_nontried` and `bytes_retryable` (= NonTrimmed +
/// NonScraped) split that aggregate so UIs can distinguish *unread*
/// territory (still ahead of Pass 1's read head) from *needs-retry*
/// territory (Pass 1 already encountered, queued for Pass 2-N).
#[derive(Debug, Clone, Copy, Default)]
pub struct MapStats {
pub bytes_total: u64,
pub bytes_good: u64,
pub bytes_unreadable: u64,
pub bytes_pending: u64,
/// Sectors Pass 1 hasn't reached yet (`NonTried`). Subset of
/// `bytes_pending`.
pub bytes_nontried: u64,
/// Sectors flagged for Pass 2-N retry — `NonTrimmed` (multi-sector
/// read failed; needs split) + `NonScraped` (small-block read
/// partially recovered; remainder still pending). Subset of
/// `bytes_pending`. This is the right signal for a "MAYBE / will
/// retry" UI bucket; `bytes_pending` over-counts because it folds
/// in `bytes_nontried`.
pub bytes_retryable: u64,
}
/// Write-through mapfile. Every `record()` persists to disk immediately
@@ -275,8 +291,13 @@ impl Mapfile {
match e.status {
SectorStatus::Finished => s.bytes_good += e.size,
SectorStatus::Unreadable => s.bytes_unreadable += e.size,
SectorStatus::NonTried | SectorStatus::NonTrimmed | SectorStatus::NonScraped => {
s.bytes_pending += e.size
SectorStatus::NonTried => {
s.bytes_pending += e.size;
s.bytes_nontried += e.size;
}
SectorStatus::NonTrimmed | SectorStatus::NonScraped => {
s.bytes_pending += e.size;
s.bytes_retryable += e.size;
}
}
}
+2 -10
View File
@@ -1393,11 +1393,7 @@ impl Disc {
// Fast path — full block read cleanly.
read_ok_count += 1;
if opts.decrypt {
crate::decrypt::decrypt_sectors(
&mut buf[..block_bytes_usz],
&keys,
0,
)?;
crate::decrypt::decrypt_sectors(&mut buf[..block_bytes_usz], &keys, 0)?;
}
file.seek(SeekFrom::Start(pos))
.map_err(|e| Error::IoError { source: e })?;
@@ -1496,11 +1492,7 @@ impl Disc {
read_ok_count += 1;
consecutive_good = consecutive_good.saturating_add(1);
if opts.decrypt {
crate::decrypt::decrypt_sectors(
&mut buf[..one_bytes],
&keys,
0,
)?;
crate::decrypt::decrypt_sectors(&mut buf[..one_bytes], &keys, 0)?;
}
file.seek(SeekFrom::Start(s_pos))
.map_err(|e| Error::IoError { source: e })?;
+4 -1
View File
@@ -241,7 +241,10 @@ impl Drive {
5_000,
) {
Ok(_) => DriveStatus::DiscPresent,
Err(ref e) if e.scsi_sense().is_some_and(|s| s.is_not_ready() || s.is_unit_attention()) => {
Err(ref e)
if e.scsi_sense()
.is_some_and(|s| s.is_not_ready() || s.is_unit_attention()) =>
{
DriveStatus::NotReady
}
_ => DriveStatus::Unknown,
+2 -10
View File
@@ -389,13 +389,7 @@ impl std::fmt::Display for Error {
s.asc,
s.ascq,
),
None => write!(
f,
"E{}: 0x{:02x}/0x{:02x}",
self.code(),
opcode,
status,
),
None => write!(f, "E{}: 0x{:02x}/0x{:02x}", self.code(), opcode, status,),
},
Error::IoError { source } => write!(f, "E{}: {}", self.code(), source),
Error::DiscRead { sector } => write!(f, "E{}: {}", self.code(), sector),
@@ -469,9 +463,7 @@ impl Error {
/// no sense data exists).
pub fn scsi_sense(&self) -> Option<&crate::scsi::ScsiSense> {
match self {
Error::ScsiError {
sense: Some(s), ..
} => Some(s),
Error::ScsiError { sense: Some(s), .. } => Some(s),
_ => None,
}
}
+4 -4
View File
@@ -134,10 +134,10 @@ pub use decrypt::{DecryptKeys, decrypt_sectors};
// background. The codec / channel / resolution enums are the canonical
// structured representation; never compare against display strings.
pub use disc::{
AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat,
DamageSeverity, Disc, DiscFormat, DiscId, DiscTitle, Extent, FrameRate, HdrFormat,
KeySource, LabelPurpose, LabelQualifier, Resolution, SampleRate, ScanOptions, Stream,
SubtitleStream, VideoStream, classify_damage,
AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat, DamageSeverity,
Disc, DiscFormat, DiscId, DiscTitle, Extent, FrameRate, HdrFormat, KeySource, LabelPurpose,
LabelQualifier, Resolution, SampleRate, ScanOptions, Stream, SubtitleStream, VideoStream,
classify_damage,
};
// ─── Streams ────────────────────────────────────────────────────────────────
+5 -1
View File
@@ -620,7 +620,11 @@ mod parse_sense_tests {
// independently of the response code. parse_sense_key must mask
// it off before classifying the format.
let s = buf(0xF2, 0x05, 0x77);
assert_eq!(parse_sense_key(&s, 8), 5, "VALID-bit must not leak into format detection");
assert_eq!(
parse_sense_key(&s, 8),
5,
"VALID-bit must not leak into format detection"
);
let s = buf(0xF0, 0x77, 0x02);
assert_eq!(parse_sense_key(&s, 18), 2);
}