From 3db41062532eeb634bad0f7a6d4e49f8aa86303e Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:04:51 -0700 Subject: [PATCH] Stop documenting the recovery API that 1.6.0 deleted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Disc::sweep, Disc::patch, Disc::copy, SweepOptions and PatchOptions have zero occurrences in src/ — recovery moved to freemkv-engine — but they were still documented in 30 places across README.md, TROUBLESHOOTING.md, six files under docs/, seven src/ doc comments and a Cargo.toml comment. README.md is the crate's GitHub front page and carried a full multi-pass code example that cannot compile. Two of the src/ references were intra-doc LINKS to deleted items ([`disc::Disc::copy`], [`disc::Disc::patch`] in scsi/mod.rs). They produced no warning on a normal `cargo doc` only because they sit on pub(crate) items; `--document-private-items` reports both, and they are gone now. The README example is deleted rather than rewritten against the engine's API: libfreemkv documenting a downstream crate's API on its own front page is the drift that produced this, and it cannot even depend on it. The src/ references become plain code spans naming freemkv_engine::recovery::* — deliberately not links, for the same reason. docs/rip-recovery.md was 202 lines about relocated code. It now documents only what this crate owns — Drive::read, SenseFamily, DiscStream's adaptive batch halving — plus the read-path design constraints, which belong with the code that enforces them, and points at freemkv-engine/src/recovery/ for the strategy. api-design.md's module tree is regenerated from the real src/disc/ and src/drive/ layouts instead of hand-patched; it had listed sweep.rs, patch.rs, mapfile.rs and read_error.rs, none of which exist. Three stale facts surfaced while rewriting and are corrected: the read timeouts are 10 s / 60 s, not the documented 1.5 s / 30 s; Drive::reset and SgIoTransport::reset no longer exist at all, so "no SCSI reset from any read path" is now stated as the stronger fact it has become; and verify_title, listed as a progress-emitting operation, was removed entirely. CHANGELOG.md keeps its references — those are the historical record of the releases that shipped the API. --- Cargo.toml | 3 +- README.md | 51 ++-------- TROUBLESHOOTING.md | 4 +- docs/README.md | 2 +- docs/api-design.md | 17 ++-- docs/architecture.md | 3 +- docs/disc-to-rip.md | 4 +- docs/drive-access.md | 7 +- docs/rip-recovery.md | 232 ++++++++++++------------------------------- src/drive/mod.rs | 8 +- src/error.rs | 2 +- src/mux/disc.rs | 2 +- src/mux/mod.rs | 2 +- src/mux/resolve.rs | 2 +- src/progress.rs | 8 +- src/scsi/mod.rs | 12 +-- 16 files changed, 114 insertions(+), 245 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 600b2a8..9721242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,8 @@ base64 = "0.22.1" # parser, not a hand-rolled scanner: the XPL is genuine XML (comments, varied # attribute order, self-closing tags). roxmltree = "0.20" -# Trace-level instrumentation for Disc::copy + SgIoTransport::execute. Permitted +# Trace-level instrumentation for the read/transport path (SgIoTransport::execute +# and friends). Permitted # under CLAUDE.md ("Acceptable strings: debug/trace logging"). Consumers (autorip) # wire a tracing subscriber and pipe events into the JSONL debug log. tracing = "0.1" diff --git a/README.md b/README.md index 409941d..f035d13 100644 --- a/README.md +++ b/README.md @@ -55,48 +55,15 @@ output.finish()?; ### Multi-pass recovery rip -For damaged discs the library exposes two flat verbs — `Disc::sweep` for the -forward Pass 1 and `Disc::patch` for retrying bad ranges. The library never -loops; the multipass policy is the caller's job. See -[`docs/rip-recovery.md`](docs/rip-recovery.md). +Recovery moved OUT of this crate in 1.6.0. The sweep/patch strategy, the +ddrescue mapfile, damage classification and the multipass loop now live in the +`freemkv-engine` crate as `freemkv_engine::recovery::{copy, sweep, patch}`. -```rust -use libfreemkv::{SweepOptions, PatchOptions}; -use libfreemkv::disc::{mapfile, mapfile_path_for}; -use std::path::Path; - -let iso = Path::new("disc.iso"); - -// Pass 1: disc → ISO. Skip-on-error, zero-fill, write the sidecar mapfile. -disc.sweep(&mut drive, iso, &SweepOptions { - decrypt: true, - resume: false, - batch_sectors: None, - skip_on_error: true, - progress: None, - halt: None, -})?; - -// Pass 2..N: retry every non-finished range. Idempotent. -loop { - let map = mapfile::Mapfile::load(&mapfile_path_for(iso))?; - let stats = map.stats(); - if stats.bytes_pending + stats.bytes_unreadable == 0 { break; } - - let outcome = disc.patch(&mut drive, iso, &PatchOptions { - decrypt: true, - block_sectors: None, - full_recovery: true, - reverse: true, - wedged_threshold: 50, - progress: None, - halt: None, - })?; - if outcome.bytes_recovered_this_pass == 0 { break; } -} - -// Mux from the ISO via the normal stream pipeline (no drive involvement). -``` +libfreemkv keeps the layers underneath: the raw single-shot read +(`Drive::read`) and the SCSI-fact translation (`SenseFamily`) that the engine's +strategy is built on. The dependency runs engine → libfreemkv, so this crate +cannot call into it; front-ends get recovery from the engine directly. See +[`docs/rip-recovery.md`](docs/rip-recovery.md) for what stayed here. ## What It Does @@ -114,7 +81,7 @@ loop { | Stream | Input | Output | Transport | |--------|-------|--------|-----------| | DiscStream | Yes | -- | Optical drive via SCSI | -| IsoStream | Yes | -- | Blu-ray ISO image file (read via stream pipeline; written via `Disc::sweep()`) | +| IsoStream | Yes | -- | Blu-ray ISO image file (read via stream pipeline; written by `freemkv_engine::recovery`) | | MkvStream | Yes | Yes | Matroska container | | M2tsStream | Yes | Yes | BD transport stream with FMKV metadata header | | NetworkStream | Yes (listen) | Yes (connect) | TCP with FMKV metadata header | diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 2ba9bbb..05a19aa 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -141,8 +141,8 @@ If your machine has a free SATA port, use it. freemkv uses a three-layer recovery model. See [`docs/rip-recovery.md`](docs/rip-recovery.md) for full details. -- **Pass 1 (Disc::copy):** Fast sweep with 64 KB reads. On failure, zero-fills the block and skips forward. Writes a ddrescue-format mapfile for later retry. -- **Pass 2+ (Disc::patch):** Targeted re-reads of bad ranges with a long 30-second timeout per CDB. The drive firmware performs its own ECC and laser power retries within that window. +- **Pass 1 (`freemkv_engine::recovery::copy`):** Fast sweep with 64 KB reads. On failure, zero-fills the block and skips forward. Writes a ddrescue-format mapfile for later retry. +- **Pass 2+ (`freemkv_engine::recovery::patch`):** Targeted re-reads of bad ranges with a long 60-second timeout per CDB. The drive firmware performs its own ECC and laser power retries within that window. - **In-stream (DiscStream):** Adaptive batch halving -- reduces request size on failure to isolate bad sectors within a larger block. This means a disc with some bad sectors will still produce a usable ISO. The damaged areas are zero-filled in pass 1 and retried in subsequent passes. Structure-protected sectors (deliberate unreadable regions from copy protection) will never yield, which is expected. diff --git a/docs/README.md b/docs/README.md index 45a5335..0adb5d0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,7 +12,7 @@ Technical documentation for [libfreemkv](https://github.com/freemkv/libfreemkv), |----------|---------------| | [Architecture](architecture.md) | Module map, design principles, error codes, platform support | | [Drive Access](drive-access.md) | Drive, SCSI transport, profiles, unlock, why raw mode is needed | -| [Rip Recovery](rip-recovery.md) | Three-layer recovery model: Disc::patch, single-shot Drive::read, DiscStream batch halving | +| [Rip Recovery](rip-recovery.md) | What this crate owns of the recovery model: single-shot Drive::read, SenseFamily, DiscStream batch halving (the strategy itself moved to freemkv-engine in 1.6.0) | | [AACS Encryption](aacs.md) | Key resolution (4 paths), content decryption, bus encryption, SCSI handshake | | [UDF Filesystem](udf.md) | UDF 2.50 with metadata partitions, pointer chain, how files are read from disc | | [MPLS Playlists](mpls.md) | Playlist format, play items, STN stream table, coding types | diff --git a/docs/api-design.md b/docs/api-design.md index 13f8832..d535192 100644 --- a/docs/api-design.md +++ b/docs/api-design.md @@ -170,17 +170,20 @@ libfreemkv/src/ │ ├── writeback_file.rs WritebackFile (was crate::io::Writer) │ └── writeback.rs sync_file_range pipeline ├── drive/ Drive (open, init, single-shot read) -│ ├── mod.rs Drive struct, init, read (single-shot), reset, eject +│ ├── mod.rs Drive struct, init, read (single-shot), eject │ ├── capture.rs Raw drive SCSI capture (INQUIRY/GET_CONFIG) for contribution │ ├── linux.rs Linux drive discovery │ ├── macos.rs macOS drive discovery │ └── windows.rs Windows drive discovery -├── disc/ Disc (scan, titles, AACS setup, sweep, patch) -│ ├── mod.rs Disc struct, scan, titles, formats; Disc::copy + Disc::sweep (Pass 1) -│ ├── sweep.rs Pass 1 internal helpers (pub(super)) -│ ├── patch.rs Disc::patch (Pass N retry over mapfile) -│ ├── mapfile.rs ddrescue-format mapfile -│ └── read_error.rs ReadCtx / ReadAction state machine +├── disc/ Disc (scan, titles, AACS setup, per-format parsing) +│ ├── mod.rs Disc struct, scan, titles, formats +│ ├── bluray.rs Blu-ray / UHD scanning (MPLS/CLPI-driven) +│ ├── dvd.rs DVD-Video scanning (IFO-driven) +│ ├── hddvd.rs HD-DVD scanning +│ ├── extract.rs Per-extent content extraction +│ ├── encrypt.rs Encrypted-range mapping for content reads +│ ├── dvd_audio_probe.rs DVD audio-stream probing +│ └── pgs_forced_probe.rs PGS forced-subtitle probing ├── scsi/ SCSI transport (Linux SG_IO, macOS IOKit, Windows SPTI) ├── unlock.rs Unlocker trait + registry (pluggable unlock seam) ├── aacs/ AACS decryption (handshake, keys, keydb, decrypt) diff --git a/docs/architecture.md b/docs/architecture.md index f64d2e1..989380c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -101,7 +101,8 @@ After open: - `eject()` -- eject tray Recovery is layered above `Drive::read`, not inside it. Layer 1 -(`Disc::patch`) handles bad-range retry by replaying the ddrescue mapfile. +(`freemkv_engine::recovery::patch`, in the engine crate) handles bad-range +retry by replaying the ddrescue mapfile. Layer 3 (`DiscStream::fill_extents` adaptive batch sizer) handles in-loop request-size adaptation. Inline recovery (gentle retry → SCSI reset → retry) was removed in 0.13.6 — see [`rip-recovery.md`](rip-recovery.md) and diff --git a/docs/disc-to-rip.md b/docs/disc-to-rip.md index 515e9e3..cfaef6d 100644 --- a/docs/disc-to-rip.md +++ b/docs/disc-to-rip.md @@ -79,7 +79,7 @@ Insert disc │ Or: read sectors → decrypt → raw bytes (for ISO output) │ Drive::read() is single-shot. DiscStream::fill_extents adapts the │ batch size on failure (halve / probe-up). Bad-range retry is layer - │ 1 above this — Disc::patch re-runs against the mapfile. + │ 1 above this — freemkv_engine::recovery::patch re-runs against the mapfile. │ ▼ PES frames → output stream (MKV, M2TS, network, etc.) @@ -123,7 +123,7 @@ output.finish()?; | aacs/ | [aacs.md](aacs.md) | Key resolution + content decrypt + bus handshake | | css/ | -- | DVD CSS cipher | | decrypt.rs | -- | Unified decrypt dispatcher (AACS/CSS/None) | -| disc/ | [rip-recovery.md](rip-recovery.md) | Disc::scan + Disc::sweep + Disc::patch + mapfile | +| disc/ | [rip-recovery.md](rip-recovery.md) | Disc::scan (sweep/patch/mapfile moved to freemkv-engine in 1.6.0) | | labels/ | -- | BD-J stream labels (5 format parsers) | | mux/ | -- | Stream implementations (7 stream types) | | pes.rs | -- | PES frame types + FrameSource / FrameSink traits | diff --git a/docs/drive-access.md b/docs/drive-access.md index a2e2ce2..bbb6c79 100644 --- a/docs/drive-access.md +++ b/docs/drive-access.md @@ -58,15 +58,16 @@ selects the per-CDB timeout: | `recovery` | Timeout | Used by | |------------|----------|------------------------------------------| -| `false` | 1.5 s | `Disc::sweep` fast skip-forward pass, `DiscStream::fill_extents` | -| `true` | 30 s | `Disc::patch` retry pass over the mapfile | +| `false` | 10 s | `freemkv_engine::recovery::sweep` fast skip-forward pass, `DiscStream::fill_extents` | +| `true` | 60 s | `freemkv_engine::recovery::patch` retry pass over the mapfile | On any SCSI failure or timeout, `read` returns `Err(DiscRead)` immediately. There are no inline retries, no SCSI reset, no Phase 1/2/3 escalation. Recovery is layered above `Drive::read`: -- **Layer 1 — `Disc::patch`** loops over the ddrescue mapfile and re-issues +- **Layer 1 — `freemkv_engine::recovery::patch`** (in the engine crate, not + here) loops over the ddrescue mapfile and re-issues `read(.., recovery=true)` against each non-`+` range. - **Layer 3 — `DiscStream::fill_extents`** halves the request size on failure, retries at the same LBA, and probes back up on a clean-read diff --git a/docs/rip-recovery.md b/docs/rip-recovery.md index 5597a6b..2556f81 100644 --- a/docs/rip-recovery.md +++ b/docs/rip-recovery.md @@ -1,136 +1,38 @@ -# Rip recovery — three-layer architecture +# Rip recovery — what libfreemkv owns -`libfreemkv` supports a multi-stage rip model for damaged or protection-bearing -discs: a fast forward sweep that tolerates read failures, in-loop request-size -adaptation that survives transient drive trouble without bailing, and targeted -retry passes against a persistent bad-range map. The stream pipeline -(`DiscStream` + `input`/`output`) operates against the resulting ISO image, so -the mux stage never touches the drive. +**Recovery strategy moved OUT of this crate in 1.6.0.** The forward sweep, the +targeted retry pass, the ddrescue mapfile, damage classification and the +multipass loop now live in the **`freemkv-engine`** crate as +`freemkv_engine::recovery::{copy, sweep, patch}`. The dependency runs +engine → libfreemkv, so this crate cannot call into the engine; front-ends +(`freemkv` CLI, autorip) get recovery from the engine directly. -Recovery is layered cleanly. Each layer has one responsibility and does not -reach into the others. +What stayed here are the two layers underneath the strategy: the single-shot +read primitive, and the in-stream request-size adaptation that sits in front of +it. This document covers those, plus the design constraints they encode — the +constraints are the reason the strategy above them looks the way it does, so +they belong with the code that enforces them. + +For the strategy itself — damage-jump thresholds, pass ordering, mapfile status +state machine, wedge detection — read `freemkv-engine/src/recovery/`. | Layer | Where it lives | What it does | |-------|---------------|--------------| -| 1 — Bad-range retry | `Disc::patch` (one pass over the mapfile per call) | Re-reads non-`+` ranges with the long timeout. Idempotent; caller invokes N times. | +| 1 — Bad-range retry | **`freemkv-engine`** (`recovery::patch`) | Re-reads non-`+` ranges with the long timeout. Idempotent; caller invokes N times. | | 2 — Single-shot primitive | `Drive::read` in `src/drive/mod.rs` | One CDB, one timeout, one result. No inline retries, no SCSI reset. | -| 3 — In-loop request adaptation | `DiscStream::fill_extents` adaptive batch sizer | Halves the batch on failure, retries at the same LBA, walks back up on a clean-read streak. | +| 3 — In-loop request adaptation | `DiscStream::fill_extents` in `src/mux/disc.rs` | Halves the batch on failure, retries at the same LBA, walks back up on a clean-read streak. | -The library exposes flat verbs; the caller drives the multipass loop. Autorip -runs `Disc::sweep` once, then loops `Disc::patch` until either the mapfile is -clean or the configured retry budget is exhausted, then hands the ISO off to -the mux pipeline. The `freemkv` CLI does the same shape, but as of 1.6.0 the -loop itself (including the multi-title rip loop) lives one layer up, in the -shared `freemkv-engine` crate, with a terminal-output progress sink plugged -into it as the `Sink`. Layer 3 runs inside any consumer of `DiscStream` -(direct PES pipeline, ISO playback, etc.) without caller involvement. +Layer 2 also translates drive facts: [`SenseFamily`](../src/scsi/mod.rs) +classifies SCSI sense data into the categories the engine's strategy routes on +(marginal vs. hardware vs. not-ready). Getting that classification wrong +silently misroutes recovery, which is why it lives next to the transport rather +than in the strategy. -Three primitives compose the disc-side flow: +Layer 3 runs inside any consumer of `DiscStream` — direct PES pipeline, ISO +playback — without caller involvement, and applies whether or not the engine's +recovery is in play. -| Primitive | What it does | -|---------------------------|-----------------------------------------------------------------------| -| `Disc::sweep` | disc → ISO, one forward pass. Writes a sidecar `.mapfile`. Opt-in skip-on-error. | -| `Disc::patch` | Re-reads bad ranges from the drive. One pass per call; caller invokes N times. | -| `DiscStream` (ISO source) | Reads sectors from the ISO, feeds decrypt → demux → codec → mux. | - -## Data model - -### Mapfile - -Format: [ddrescue](https://www.gnu.org/software/ddrescue/manual/ddrescue_manual.html)-compatible -plain text, greppable, tool-interoperable. Flushed to disk on every `record()` -so a crashed rip loses at most one block. - -``` -# Rescue Logfile. Created by libfreemkv v0.13.6 -# Current pos / status / pass / pass_time -0x000000000 ? 1 0 -# pos size status -0x000000000 0x12a35d000 + -0x12a35d000 0x000003000 - -0x12a360000 0x009c4a000 + -0x12d00a000 0x000064000 * -``` - -Status characters match ddrescue: - -| Char | Meaning | -|------|----------------------------------------------------| -| `?` | Not yet attempted | -| `*` | Fast-pass failed; needs edge-trim | -| `/` | Trimmed; interior needs sector scrape | -| `-` | Unreadable this session | -| `+` | Finished (good) | - -Position and size are hex byte offsets into the ISO. - -### `SweepOptions` and `PatchOptions` - -The library no longer dispatches between sweep and patch internally — the -caller picks the verb explicitly per pass. The two option structs are flat -and have no overlap: - -```rust -SweepOptions { - decrypt: true, - resume: false, - batch_sectors: None, - skip_on_error: true, // damage-jump + zero-fill on read failure - progress: Some(&reporter), - halt: Some(flag), -} - -PatchOptions { - decrypt: true, - block_sectors: None, - full_recovery: true, - reverse: true, // walk bad ranges high → low LBA - wedged_threshold: 50, - progress: Some(&reporter), - halt: Some(flag), -} -``` - -Caller-orchestrated dispatch (the policy `Disc::copy` used to embed): - -- No mapfile → `sweep` (fresh Pass 1). -- Mapfile with `?` ranges → `sweep` with `resume: true`. -- Mapfile covers full disc, only `*` / `/` / `-` ranges → `patch`. -- Mapfile clean → done; no further pass needed. - -Each consumer (autorip, `freemkv` CLI) implements the loop in roughly five -lines of `Mapfile::stats()` checks. - -## Algorithm - -### Pass 1 — fast sweep (`Disc::sweep`) - -1. Read one ECC block (32 sectors for UHD, 16 for BD/DVD) at the current LBA. -2. On success: write data to ISO, mark `+`, advance. -3. On failure (with `multipass`): zero-fill, mark `*`, advance. -4. Track a sliding window of the last 16 ECC block results. When ≥12% are failures - → **damage-jump**: skip ahead by `1024×batch×multiplier` sectors (64 MB base for - UHD). Double the multiplier on each jump (64→128→256→512 MB...). Zero-fill the gap as `*`. -5. On 16 consecutive good reads: reset jump multiplier to 1, restore max read speed. -6. Speed control: damage zone entry → minimum speed, exit → maximum speed. -7. Only transport failures (USB bridge crash) abort the pass. - -Pass 1 completes when every byte has been visited (either `+` or `*`). - -### Pass 2+ — patch (`Disc::patch`) - -`Disc::patch` reads the mapfile and iterates every non-`+` range. Default: **reverse** mode -(walks ranges from highest LBA to lowest, within each range from end to start). - -1. Issue a single-sector read with 60 s timeout (`recovery=true`). Drive firmware - does its own ECC recovery inside that window. -2. On success: write the good bytes into the ISO, mark `+`. -3. On failure with non-marginal SCSI sense: bail immediately (drive won't produce data). -4. On failure with marginal sense: mark `-`, continue. -5. Update the mapfile after every block — crash-safe resume. -6. Wedged-drive exit: 50 consecutive failures with zero recovery → bail this pass. - -### In-stream — adaptive batch halving (`DiscStream::fill_extents`) +## In-stream — adaptive batch halving (`DiscStream::fill_extents`) When a consumer reads a `DiscStream` directly (no ISO intermediate), `fill_extents` runs an adaptive sizer in front of `Drive::read`: @@ -144,59 +46,53 @@ When a consumer reads a `DiscStream` directly (no ISO intermediate), `EventKind::SectorSkipped`) when `skip_errors` is set, otherwise return `Err(DiscRead)`. -This is layer 3. It exists so a transient single-sector glitch in a 32-sector -batch can be isolated and read individually without the caller needing to -implement retry logic. +This exists so a transient single-sector glitch inside a 32-sector batch can be +isolated and read individually without the caller implementing retry logic. See +[`src/event.rs`](../src/event.rs) for the emitted events. ## Design choices -**`Drive::read` is single-shot.** No inline retry phases, no SCSI reset, -no eject cycle. The `recovery` flag controls only the per-CDB timeout -(1.5 s vs. 30 s); on any failure it returns `Err(DiscRead)` immediately. -Inline recovery (5× gentle retry → close + SCSI reset + reopen → 5× more) -was removed in 0.13.6. See the stop-wedge postmortem (2026-04-25) for rationale: -the inline reset on the LG BU40N (Initio USB-SATA bridge) -wedged drive firmware below the bridge without ever recovering a sector, -and the gentle-retry phase produced long stretches of 0 KB/s with no -recoveries to show for it. Recovery responsibility is now layered: layer 1 -handles ranges, layer 3 handles request size, neither touches the -wedge-prone reset path. +These are constraints on the read path, enforced here and relied on by the +engine's strategy. -**No `MODE SELECT` to disable drive retries.** Neither ddrescue -nor any consumer ripper does this. Drive firmware has access to raw analog signal, laser -power control, and drive-specific ECC tuning that userspace can't replicate — -disabling it throws away recovery headroom on marginal sectors. We fail fast -via short SG_IO timeouts in pass 1 and let the firmware work the long timeout -in pass 2 / patch. +**`Drive::read` is single-shot.** No inline retry phases, no SCSI reset, no +eject cycle. The `recovery` flag controls only the per-CDB timeout (10 s vs. +60 s); on any failure it returns `Err(DiscRead)` immediately. Inline recovery +(5× gentle retry → close + SCSI reset + reopen → 5× more) was removed in +0.13.6. See the stop-wedge postmortem (2026-04-25) for rationale: the inline +reset on the LG BU40N (Initio USB-SATA bridge) wedged drive firmware below the +bridge without ever recovering a sector, and the gentle-retry phase produced +long stretches of 0 KB/s with nothing to show for it. Recovery responsibility +is layered instead: layer 1 handles ranges, layer 3 handles request size, +neither touches the wedge-prone reset path. -**No SCSI reset from any retry path.** `SgIoTransport::reset` (Linux) is -trimmed to a kernel SG_IO state flush plus ALLOW MEDIUM REMOVAL — the -`SG_SCSI_RESET` ioctl and STOP/START UNIT escalation were removed in 0.13.6. -The macOS reset (which had been a no-op) was removed entirely. The top-level -`scsi::reset()` / `reset_with_timeout()` / `reset_blocking()` wrappers were -also removed (no callers). The remaining `Drive::reset()` is only invoked -explicitly by callers that need an eject-cycle escape hatch — it is never -reached from a read path. +**No `MODE SELECT` to disable drive retries.** Neither ddrescue nor any +consumer ripper does this. Drive firmware has access to raw analog signal, +laser power control and drive-specific ECC tuning that userspace cannot +replicate — disabling it throws away recovery headroom on marginal sectors. The +fast pass fails quickly via short SG_IO timeouts and lets the firmware work the +long timeout during retry. -**ISO intermediate, even for single-pass.** Pass 1 always writes an ISO. The -mux stage reads the ISO via `FileSectorSource`. For single-pass (no retries), -this adds ~2-3 min (local disk mux) but gains resumability across crashes, +**No SCSI reset from any read path.** There is no reset escape hatch on +`Drive` at all: the `SG_SCSI_RESET` ioctl and STOP/START UNIT escalation went in +0.13.6, the macOS reset (always a no-op) was removed entirely, and the +top-level `scsi::reset()` wrappers went with their last callers. The only +remaining reset is a Windows-specific device-level helper in +[`src/scsi/windows.rs`](../src/scsi/windows.rs), never reached from a read. + +**ISO intermediate, even for single-pass.** The engine's Pass 1 always writes +an ISO, and the mux stage reads it back via `FileSectorSource`. For a +no-retry rip this costs a few minutes but buys resumability across crashes, re-muxability without re-ripping, and a persistent forensic artifact. Callers -who need pure speed can bypass and use `DiscStream::new(Box::new(drive), …)` -directly — the lib doesn't forbid it, and layer 3 (adaptive batch halving) -still applies there. - -**Mapfile in ddrescue format.** Plain text so users can `less` it, `diff` it, -or feed it to ddrescue's own tooling. Crash-safe (flush-per-record). Entries -coalesce on adjacent same-status ranges so files stay small. - -**Patches target `-`, `*`, `/`, and `?` alike.** The status state machine is -ddrescue's but `patch` collapses the distinction — it just tries every -non-finished range with the long timeout. Future work can specialize (trim vs. -scrape vs. retry with direction reversal) if there's measured benefit. +who need pure speed can bypass it with `DiscStream::new(Box::new(drive), …)` — +nothing forbids it, and layer 3 still applies there. ## References - [ddrescue manual, Algorithm chapter](https://www.gnu.org/software/ddrescue/manual/ddrescue_manual.html) - [ddrescue optical media notes](https://www.electric-spoon.com/doc/gddrescue/html/Optical-media.html) -- Source: [`src/disc/mapfile.rs`](../src/disc/mapfile.rs), [`src/disc/mod.rs`](../src/disc/mod.rs) (`Disc::sweep`), [`src/disc/patch.rs`](../src/disc/patch.rs) (`Disc::patch`), [`src/drive/mod.rs`](../src/drive/mod.rs) (`Drive::read`), [`src/mux/disc.rs`](../src/mux/disc.rs) (`DiscStream::fill_extents`). +- Recovery strategy and mapfile: `freemkv-engine/src/recovery/` +- In this crate: [`src/drive/mod.rs`](../src/drive/mod.rs) (`Drive::read`), + [`src/scsi/mod.rs`](../src/scsi/mod.rs) (`SenseFamily`), + [`src/mux/disc.rs`](../src/mux/disc.rs) (`DiscStream::fill_extents`), + [`src/event.rs`](../src/event.rs) (progress events). diff --git a/src/drive/mod.rs b/src/drive/mod.rs index 6fd3742..3ca261e 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -715,19 +715,19 @@ impl Drive { /// SCSI reset. /// /// `recovery=true` uses [`crate::scsi::READ_RECOVERY_TIMEOUT_MS`] (60 s, - /// matches sg_dd) for the `Disc::patch` pass; `recovery=false` uses - /// [`crate::scsi::READ_TIMEOUT_MS`] (10 s) for `Disc::copy`'s fast + /// matches sg_dd) for the `freemkv_engine::recovery::patch` pass; `recovery=false` uses + /// [`crate::scsi::READ_TIMEOUT_MS`] (10 s) for `freemkv_engine::recovery::copy`'s fast /// skip-forward sweep. Both budgets are generous enough that the drive /// can finish ECC recovery on a marginal sector — pre-0.13.21 this was /// 1.5 s on the fast path which forced the kernel mid-layer to time /// out and escalate while we waited anyway. On any failure returns - /// `Err(DiscRead)` immediately; orchestration (`Disc::patch` multi-pass, + /// `Err(DiscRead)` immediately; orchestration (`freemkv_engine::recovery::patch` multi-pass, /// `DiscStream` adaptive batch halving) handles retry policy. /// /// Inline retry phases (5× gentle + reset+reopen + 5× more) were /// removed in 0.13.6: on some USB-SATA bridges the inline reset wedged /// drive firmware without ever recovering a sector. The remaining - /// recovery layers (Disc::patch multi-pass, DiscStream batch halving) + /// recovery layers (freemkv_engine::recovery::patch multi-pass, DiscStream batch halving) /// do not touch the wedge-prone reset path. pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool) -> Result { // Bulk path: FUA off (the drive cache IS the streaming throughput). diff --git a/src/error.rs b/src/error.rs index a0b8806..06af469 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1032,7 +1032,7 @@ impl Error { /// REQUEST, BLANK CHECK, kernel `IoError`, and any non-SCSI variant. /// Caller-agnostic predicate — describes a property of the *error*, /// not what one specific call site should do with it. Used by - /// `Disc::copy`'s hysteresis dispatch. + /// `freemkv_engine::recovery::copy`'s hysteresis dispatch. pub fn is_marginal_read(&self) -> bool { self.scsi_sense() .map(crate::scsi::ScsiSense::is_marginal) diff --git a/src/mux/disc.rs b/src/mux/disc.rs index 884ce08..a334542 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -3,7 +3,7 @@ //! One stream type for all disc sources. The source is a SectorSource — //! Drive (hardware) or FileSectorSource (file). DiscStream doesn't care. //! -//! Read-only. For disc→ISO (raw sector copy), use `Disc::copy()`. +//! Read-only. For disc→ISO (raw sector copy), use `freemkv_engine::recovery::copy`. use crate::disc::{DiscTitle, Extent}; use crate::drive::extract_scsi_context; diff --git a/src/mux/mod.rs b/src/mux/mod.rs index 16e5604..1fbee88 100644 --- a/src/mux/mod.rs +++ b/src/mux/mod.rs @@ -21,7 +21,7 @@ //! output.finish()?; //! ``` //! -//! For disc→ISO (raw sector copy), use `Disc::copy()` instead. +//! For disc→ISO (raw sector copy), use `freemkv_engine::recovery::copy` instead. // Public modules — types here are intentionally part of the consumable API. pub mod disc; diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index ac19776..8009511 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -16,7 +16,7 @@ //! | fvi:// | -- | Yes | file path (required) — per-picture video index | //! //! Bare paths without a scheme are rejected. -//! For disc→ISO (raw sector copy), use `Disc::copy()` instead. +//! For disc→ISO (raw sector copy), use `freemkv_engine::recovery::copy` instead. //! //! Note: `disc://` cannot be opened through [`input`]; it returns //! [`crate::error::Error::DiscUrlNotDirect`]. Live-disc input must go diff --git a/src/progress.rs b/src/progress.rs index f1e6b23..d49ae45 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -1,7 +1,7 @@ //! Pipeline-progress reporting for the rip pipeline. //! //! Architecture rule: ONE progress signal type. Every long-running -//! pipeline operation (`Disc::copy`, `Disc::patch`, `verify_title`) emits the +//! pipeline operation (`freemkv_engine::recovery::{copy, patch}`) emits the //! same [`PassProgress`] shape via the [`Progress`] trait. Consumers (autorip) //! compute their own single derived view from these fields and never reach //! into per-pass internals. @@ -18,12 +18,12 @@ /// (reverse)", "Scrape", "Mux") or just use a generic "Pass N" label. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PassKind { - /// `Disc::copy` — initial sweep across the entire disc. + /// `freemkv_engine::recovery::copy` — initial sweep across the entire disc. Sweep, - /// `Disc::patch` retry pass with `block_sectors >= 2`. `reverse=true` + /// `freemkv_engine::recovery::patch` retry pass with `block_sectors >= 2`. `reverse=true` /// means walking bad ranges from highest to lowest LBA. Trim { reverse: bool }, - /// `Disc::patch` final pass at 1 sector per block. + /// `freemkv_engine::recovery::patch` final pass at 1 sector per block. Scrape { reverse: bool }, /// Demux ISO → output (MKV / M2TS / network). Single phase that runs /// after all rip passes complete. The library's mux pipeline does not diff --git a/src/scsi/mod.rs b/src/scsi/mod.rs index 042f099..c0d05a3 100644 --- a/src/scsi/mod.rs +++ b/src/scsi/mod.rs @@ -44,7 +44,7 @@ pub const AACS_KEY_CLASS: u8 = 0x02; pub(crate) const TUR_TIMEOUT_MS: u32 = 5_000; /// Timeout for content READ commands (READ_10 / READ_12) on the fast -/// path — the [`disc::Disc::copy`] sweep that bisects-on-failure. +/// path — the `freemkv_engine::recovery::copy` sweep that bisects-on-failure. /// /// 10 s is calibrated from live empirical data on an LG BU40N + Initio /// 1618L bridge ripping a UHD with marginal sectors: @@ -67,7 +67,7 @@ pub(crate) const TUR_TIMEOUT_MS: u32 = 5_000; pub(crate) const READ_TIMEOUT_MS: u32 = 10_000; /// Timeout for content READ commands on the recovery path — -/// [`disc::Disc::patch`]'s targeted retries on bad ranges. Matches +/// `freemkv_engine::recovery::patch`'s targeted retries on bad ranges. Matches /// `sg_dd`'s 60 s ceiling: long enough that any sector the drive can /// recover at all gets the time to do so, short enough that an /// unresponsive bus is detected before the per-range watchdog fires. @@ -77,7 +77,7 @@ pub(crate) const READ_TIMEOUT_MS: u32 = 10_000; /// safety ceiling, not a steady-state cost. /// /// Historical note (2026-05-08): briefly lowered to 2 s with a 5× -/// inline retry loop in `Disc::patch` to mimic the kernel `sr_mod` +/// inline retry loop in `freemkv_engine::recovery::patch` to mimic the kernel `sr_mod` /// driver's auto-retry pattern. The synthetic logic worked but on the /// live drive each "2 s" read paid ~1.5 s of kernel SCSI mid-layer /// error escalation on top, so 5× retries took ~17 s per LBA and @@ -237,7 +237,7 @@ impl ScsiSense { /// /// `false` for HARDWARE ERROR, DATA PROTECT, UNIT ATTENTION, /// ILLEGAL REQUEST, BLANK CHECK, and any unknown key. Used - /// by [`Error::is_marginal_read`] / `Disc::copy`'s hysteresis + /// by [`Error::is_marginal_read`] / `freemkv_engine::recovery::copy`'s hysteresis /// dispatch. pub fn is_marginal(&self) -> bool { matches!( @@ -1022,8 +1022,8 @@ mod parse_sense_tests { #[cfg(test)] mod scsi_sense_predicate_tests { //! Classification of [`ScsiSense`] predicate methods against SPC-4 - //! §4.5.6 Table 28 sense keys. These drive `Disc::copy` hysteresis - //! and `Disc::patch` routing; a misclassification here silently + //! §4.5.6 Table 28 sense keys. These drive `freemkv_engine::recovery::copy` hysteresis + //! and `freemkv_engine::recovery::patch` routing; a misclassification here silently //! changes which sectors get retried vs. marked unreadable. use super::*;