diff --git a/CHANGELOG.md b/CHANGELOG.md index 02f0579..e085a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## 0.16.1 (2026-04-30) + +### Unified progress display for sweep and patch + +- Patch passes now show `bytes_good / disc_total` as GB (grows when sectors are recovered), with pass progress `%` using `work_done / work_total` (always advances). `Xs unreadable` declines as data is recovered. Same UI for all pass types. + +## 0.16.0 (2026-04-30) + +### IOKit registry-based drive enumeration, BSD name matching, reverse patch default + +- **`shim_open_exclusive`** now matches the correct `IOBDServices` for the requested BSD name. Walks all IOBDServices entries in the IOKit registry, matches child IOMedia `"BSD Name"` property. Falls back to IOMedia parent walk, then first-match. Fixes multi-drive systems. +- **`shim_list_drives`** (new): registry-based drive enumeration. Reads IOBDServices `"Device Characteristics"` for vendor/model/firmware and child IOMedia for BSD name. Zero SCSI, zero exclusive access, zero unmounts. Fixes the enumeration blast that unmounted every disk on the system. +- **`list_drives()`** and **`find_drives()`** rewritten to use `shim_list_drives`. No longer iterates `/dev/disk0..15` opening exclusive SCSI on each. +- **`patch_internal`** defaults `reverse: true`. Sweep jumps forward with escalating gaps, so NonTrimmed ranges have good data at their tail. Reverse patch hits good data first, converges on actual bad block boundaries. + +## 0.15.1 (2026-04-30) + +### Fix damage-jump detection, fix dispatch covers_disc check + +- Damage-jump tuning: `DAMAGE_WINDOW=16`, `DAMAGE_THRESHOLD_PCT=12%` (was 50/25%). Old params were too diluted by good reads between sparse failures; new params trigger on 2nd scattered failure. +- `Disc::copy()` dispatch: `covers_disc` now checks `map.total_size() == disc_size` (byte-for-byte, not approximate). Fixes false sweep dispatch when mapfile exists but doesn't cover full disc. +- Sweep resume: when dispatched from existing mapfile with NonTried, passes `resume: true`. + +## 0.15.0 (2026-04-30) + +### Multipass dispatch rewrite, speed control on damage zone entry/exit + +- `CopyOptions { decrypt, multipass, progress, halt }`. `Disc::copy()` auto-detects sweep vs patch from mapfile state: no mapfile → sweep, NonTried → sweep with resume, only NonTrimmed/NonScraped/Unreadable → patch, clean → no-op. +- `Disc::mapfile_for()`: `/dev/null` output → `/tmp/.mapfile`, otherwise `mapfile_path_for(path)`. +- Speed control: damage zone entry → `set_speed(0x0000)` (minimum), 16 consecutive good reads → `set_speed(0xFFFF)` (maximum). Drive manages optimal speed in clean sections. +- `SET CD SPEED` SCSI command via `SectorReader::set_speed()` (default no-op, Drive impl sends SCSI). + +## 0.14.0 (2026-04-30) + +### Damage-jump algorithm replaces probe, bridge degradation detection + +- Damage-jump algorithm: when damage threshold exceeded in sliding window, jump ahead by `256×batch×multiplier` sectors. Doubles multiplier on each jump. Zero-fills gap as NonTrimmed. +- `ecc_sectors()`: returns ECC block size per disc format (32 for UHD, 16 for BD, 16 for DVD). +- Bridge degradation detection: NOT READY with sense key 2/ASC 0x04/ASCQ 0x3E triggers 10s cooldown, up to 5 times before treating as bad sector. + ## 0.13.43 (2026-04-29) ### Pass 1 transport-failure recovery loop diff --git a/docs/drive-access.md b/docs/drive-access.md index a929cd6..af20ca7 100644 --- a/docs/drive-access.md +++ b/docs/drive-access.md @@ -99,7 +99,6 @@ descriptors or calls ioctls outside of a `ScsiTransport` implementation. |----------|---------------|--------| | Linux | `SgIoTransport` — async `write`/`poll`/`read` on `/dev/sg*` | `/dev/sg*` | | macOS | `MacScsiTransport` — IOKit SCSITask | IOKit service | -| Windows | `WindowsScsiTransport` — SPTI | `\\.\CdRomN` | The Linux backend uses the sg driver's asynchronous interface: `write()` submits the command, `poll()` waits with an enforceable wall-clock timeout, `read()` @@ -107,6 +106,17 @@ retrieves the result. If `poll()` times out, the fd is abandoned (closed in a background thread) and a fresh fd opened — the kernel's USB error recovery cannot block us. Opens with `O_RDWR | O_NONBLOCK`. +The macOS backend uses a C shim (`macos_shim.c`) for IOKit exclusive access. +The shim handles: +1. `shim_open_exclusive(bsd_name)` — unmounts the target device via `diskutil`, + then walks the IOKit registry to find the `IOBDServices` matching the + requested BSD name (IOBDServices → IOBDBlockStorageDriver → IOMedia → "BSD Name"), + then creates MMCDeviceInterface → SCSITaskDeviceInterface → ObtainExclusiveAccess. +2. `shim_list_drives()` — registry-based enumeration with zero SCSI, zero exclusive + access, zero unmounts. Reads IOBDServices "Device Characteristics" for + vendor/model/firmware and child IOMedia "BSD Name" for the device path. +3. `shim_execute()` / `shim_close()` — raw CDB dispatch and cleanup. + On non-zero SCSI status, the transport parses sense key from the sense buffer and returns `Error::ScsiError`. diff --git a/docs/rip-recovery.md b/docs/rip-recovery.md index 4ec6ee6..e72ce1c 100644 --- a/docs/rip-recovery.md +++ b/docs/rip-recovery.md @@ -62,52 +62,51 @@ Position and size are hex byte offsets into the ISO. ### `CopyOptions` and `PatchOptions` -Defaults preserve pre-`0.11.21` behavior — abort on the first unreadable -sector. Opt in to the recovery-friendly path: +`Disc::copy()` auto-detects the pass from mapfile state: ```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() + decrypt: true, // decrypt AACS/CSS sectors + multipass: true, // enable mapfile + skip-on-error + damage-jump + progress: Some(&reporter), // progress callback + halt: Some(flag), // halt flag for graceful stop } ``` +Dispatch logic: +- No mapfile → sweep (fresh Pass 1) +- Mapfile with NonTried (`?`) → sweep with resume +- Mapfile covering full disc, only NonTrimmed/NonScraped/Unreadable → patch +- Mapfile clean → no-op + ## Algorithm -### Pass 1 — fast sweep (`Disc::copy`) +### Pass 1 — fast sweep (`Disc::copy` → `sweep_internal`) -1. Read 64 KB (32 sectors, one BD ECC block) at the current LBA via - `Drive::read(.., recovery=false)` — short 1.5 s timeout, single shot. -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. +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 `256×batch×multiplier` sectors (8 MB base for + UHD). Double the multiplier on each jump (8→16→32→64 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 terminal status (`+`, `-`, or the caller -bails via the halt flag). +Pass 1 completes when every byte has been visited (either `+` or `*`). -### Pass 2+ — patch (`Disc::patch`) +### Pass 2+ — patch (`Disc::copy` → `patch_internal`) -`Disc::patch` reads the mapfile and iterates every non-`+` range. For each: +`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 drive read via `Drive::read(.., recovery=true)` — long 30 s - timeout, still single shot. Drive firmware does its own ECC and retries - inside that window; userspace does not pile on additional retries here. -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). +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`)