- 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.
128 lines
5.5 KiB
Markdown
128 lines
5.5 KiB
Markdown
# 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`)
|