From bd5b7795bd53a1b43d02f4fc775b39080b970184 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:53:28 -0700 Subject: [PATCH] =?UTF-8?q?docs:=20multi-pass=20recovery=20=E2=80=94=20REA?= =?UTF-8?q?DME=20snippet=20+=20new=20rip-recovery.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README quick-start gains a multi-pass example using the new Disc::copy + Disc::patch primitives. - New docs/rip-recovery.md documents the two-stage rip model: mapfile format (ddrescue-compatible), CopyOptions/PatchOptions surface, the pass-1/pass-2 algorithm, and the design decisions (why no MODE SELECT, why ISO intermediate, why ddrescue mapfile). No code change. --- README.md | 25 +++++++++ docs/rip-recovery.md | 127 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 docs/rip-recovery.md diff --git a/README.md b/README.md index 9117d33..15e0db3 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,31 @@ while let Ok(Some(frame)) = input.read() { output.finish()?; ``` +### Multi-pass recovery rip + +For damaged discs, the library offers a two-stage rip model: fast sweep with zero-fill and a ddrescue-format mapfile, then targeted retry of bad ranges. See [`docs/rip-recovery.md`](docs/rip-recovery.md) for the full architecture. + +```rust +use libfreemkv::disc::{CopyOptions, PatchOptions}; + +// Pass 1: disc → ISO. Fast 64 KB reads, skip-forward on failure, +// zero-fill bad blocks, write a sidecar .mapfile. +let mut result = disc.copy( + &mut drive, + Path::new("disc.iso"), + &CopyOptions { skip_on_error: true, skip_forward: true, ..Default::default() }, +)?; + +// Pass 2..N: retry bad ranges with full drive recovery. +// Idempotent — call as many times as you want. +while result.bytes_unreadable + result.bytes_pending > 0 { + let pr = disc.patch(&mut drive, Path::new("disc.iso"), &PatchOptions::default())?; + if pr.bytes_recovered_this_pass == 0 { break; } +} + +// Then mux from the ISO via the normal stream pipeline (no drive involvement). +``` + ## What It Does - **Drive access** — open, identify, unlock, firmware upload, speed calibration, eject diff --git a/docs/rip-recovery.md b/docs/rip-recovery.md new file mode 100644 index 0000000..a2152d5 --- /dev/null +++ b/docs/rip-recovery.md @@ -0,0 +1,127 @@ +# Rip recovery — multi-pass architecture + +`libfreemkv` supports a two-stage rip model for damaged or protection-bearing +discs: a fast forward sweep that tolerates read failures, followed by 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. + +Three primitives compose the flow: + +| Primitive | What it does | +|---------------------------|-----------------------------------------------------------------------| +| `Disc::copy` | disc → ISO. Writes a sidecar `.mapfile`. Opt-in skip-forward on failure. | +| `Disc::patch` | Re-reads bad ranges from the drive. Idempotent; call N times. | +| `DiscStream` (ISO source) | Reads sectors from the ISO, feeds decrypt → demux → codec → mux. | + +The caller orchestrates. Autorip's `rip_disc` loops `copy` then N × `patch` per +the `MAX_RETRIES` config, then hands the ISO off to the existing mux pipeline. + +## 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.11.22 +# 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. + +### `CopyOptions` and `PatchOptions` + +Defaults preserve pre-`0.11.21` behavior — full drive recovery on every read, +abort on the first unreadable sector. Opt in to the recovery-friendly path: + +```rust +CopyOptions { + skip_on_error: true, // zero-fill bad blocks, continue + skip_forward: true, // exponential skip-forward after a failure + resume: true, // pick up from an existing ISO + mapfile + decrypt: false, // keep the ISO a raw disc image + ..Default::default() +} +``` + +## Algorithm + +### Pass 1 — fast sweep + +1. Read 64 KB (32 sectors, one BD ECC block) at the current LBA. +2. On success: mark the range `+`, advance by one block. +3. On failure (with `skip_on_error`): zero-fill the block in the ISO, mark + it `*`, advance. +4. If `skip_forward` is set: after a failure, jump ahead by an exponentially + growing amount (256 KB initial, doubling on consecutive failures, capped at + 1% of disc). The skipped bytes are also marked `*` — `patch` will visit + them later. +5. Reset the skip size to 256 KB on the first success after a failure. + +Pass 1 completes when every byte has terminal status (`+`, `-`, or the caller +bails via the halt flag). + +### Pass 2+ — patch + +`Disc::patch` reads the mapfile and iterates every non-`+` range. For each: + +1. Issue a drive read with full recovery enabled (SCSI-level retries, + ECC recovery, the lot). +2. On success: write the good bytes into the ISO at the exact byte offset, + mark `+`. +3. On failure: mark `-`. +4. Update the mapfile after every block — crash-safe resume. + +Idempotent. Call `patch` N times for N retry attempts; typically the caller +stops early if a pass recovers zero bytes (structure-protected sectors will +never yield). + +## Design choices + +**No `MODE SELECT` to disable drive retries.** Research showed neither ddrescue +nor MakeMKV 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 instead, and we avoid per-sector probing on first +contact by using large blocks + skip-forward. + +**ISO intermediate, even for single-pass.** Pass 1 always writes an ISO. The +mux stage reads the ISO via `IsoSectorReader`. For single-pass (no retries), +this adds ~2-3 min (local disk mux) but gains 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. + +**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 full recovery. Future work can specialize (trim vs. +scrape vs. retry with direction reversal) if there's measured benefit. + +## 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::copy`, `Disc::patch`)