The library's public-facing docs were sitting on the 0.17 trait
surface — Disc::copy, pes::Stream, SectorReader, etc. — even though
all in-tree callers migrated in 0.18 rounds 1-3. With 0.18.1 about
to ship, a user copy-pasting the README sample from crates.io would
have hit a compile error.
This commit is purely doc-side:
- README.md: Quick Start rewritten onto Disc::sweep + Disc::patch
with caller-orchestrated multipass; Streams table footnote and
Architecture row reference FrameSource / FrameSink.
- CHANGELOG.md: 0.18.1 entry describing the redesign — primitives,
trait splits, deprecations (kept alive through 0.18.x, deletion
target 0.18.2), throughput numbers.
- docs/{rip-recovery,api-design,architecture,disc-to-rip,
drive-access,udf}.md: every Disc::copy / pes::Stream /
SectorReader reference updated to the 0.18 trait surface.
- FEATURES.md: deleted (8+ versions stale; capabilities live in
README.md and CHANGELOG.md now, matching the workspace-top
FEATURES.md removal in 84acd65).
- examples/iso_dump.rs: verified compiles against 0.18.1.
No code changes.
See (internal)/memory/0_18_redesign.md.
Single contributor: MattJackson.
10 KiB
Drive Access and Unlock
Technical reference for how libfreemkv opens, identifies, unlocks, and reads optical drives.
Drive
Drive is the primary API. It owns the SCSI transport, the matched
drive profile, and the chipset-specific platform driver.
Opening a Drive
let mut drive = Drive::open(Path::new("/dev/sg4"))?;
open() performs: open device → send INQUIRY → match profile → instantiate
platform driver. The drive is ready for wait_ready() and init().
Drive Operations
| Method | Description |
|---|---|
wait_ready() |
Wait for disc insertion (30s timeout, TUR polling) |
init() |
Firmware upload + unlock + speed calibration |
probe_disc() |
Probe disc surface for optimal speeds |
read(lba, count, buf, recovery) |
Read sectors. Single-shot — no inline retries or reset. |
reset() |
Eject-cycle escape hatch. Caller-invoked only; not on the read path. |
lock_tray() |
Prevent tray ejection during rip |
unlock_tray() |
Allow tray ejection (also runs on Drop) |
eject() |
Eject disc tray |
drive_status() |
Query physical state (disc present, tray open, etc.) |
has_profile() |
Whether a bundled profile matched |
close() |
Consume Drive, cleanup (also runs via Drop) |
init() Sequence
init() orchestrates the full drive unlock:
- Platform driver
run_init()— sends vendor-specific SCSI commands - If firmware upload needed: upload, wait 10s for drive reset, retry
- Speed calibration after unlock
- Max 3 attempts before giving up
read() — single-shot
Drive::read(lba, count, buf, recovery) is the single read method. It issues
exactly one READ(10) CDB and returns the result. The recovery parameter only
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 |
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::patchloops over the ddrescue mapfile and re-issuesread(.., recovery=true)against each non-+range. - Layer 3 —
DiscStream::fill_extentshalves the request size on failure, retries at the same LBA, and probes back up on a clean-read streak.
Inline recovery (5× gentle retry → close + reset + reopen → 5× more) was
removed in 0.13.6. See the stop-wedge postmortem (2026-04-25)
for rationale: the inline reset wedged drive firmware on the LG BU40N (Initio
USB-SATA bridge) without ever recovering a sector. See
rip-recovery.md for the full three-layer model.
SCSI Transport
Trait
pub trait ScsiTransport: Send {
fn execute(
&mut self,
cdb: &[u8],
direction: DataDirection,
data: &mut [u8],
timeout_ms: u32,
) -> Result<ScsiResult>;
}
All drive communication goes through this trait. The library never opens file
descriptors or calls ioctls outside of a ScsiTransport implementation.
Platform Backends
| Platform | Implementation | Device |
|---|---|---|
| Linux | SgIoTransport — async write/poll/read on /dev/sg* |
/dev/sg* |
| macOS | MacScsiTransport — IOKit SCSITask |
IOKit service |
The Linux backend uses the sg driver's asynchronous interface: write() submits
the command, poll() waits with an enforceable wall-clock timeout, read()
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:
shim_open_exclusive(bsd_name)— unmounts the target device viadiskutil, then walks the IOKit registry to find theIOBDServicesmatching the requested BSD name (IOBDServices → IOBDBlockStorageDriver → IOMedia → "BSD Name"), then creates MMCDeviceInterface → SCSITaskDeviceInterface → ObtainExclusiveAccess.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.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.
SgIoTransport::reset (Linux) does pure userspace state cleanup: an open +
close pair to make the kernel cancel any SG_IO commands queued against a
previous fd, a 2 s sleep to let the kernel finish that cancellation, then a
fresh fd to send ALLOW MEDIUM REMOVAL to clear any stale tray lock. It does
NOT issue SG_SCSI_RESET or escalate via STOP+START UNIT. Both were tried
in 0.13.0–0.13.5 against the LG BU40N (Initio USB-SATA bridge); both failed
to recover wedged drives and made the wedge worse. The macOS reset (which
had been a no-op) was removed entirely in 0.13.6, and the top-level
scsi::reset() / reset_with_timeout() / reset_blocking() wrappers were
removed at the same time (no callers).
CDB Builders
The scsi module provides platform-agnostic CDB constructors:
| Function | CDB | Use |
|---|---|---|
inquiry() |
INQUIRY (0x12) | Drive identification |
get_config_010c() |
GET CONFIGURATION (0x46) | Feature 010C firmware date |
build_read_buffer() |
READ BUFFER (0x3C) | All platform commands |
build_set_cd_speed() |
SET CD SPEED (0xBB) | Speed control |
build_read10_raw() |
READ(10) (0x28) with flag 0x08 | Raw sector reads |
Drive Identification
DriveId::from_drive() sends two standard SCSI commands and extracts identity
fields:
| Field | Source | SCSI Reference |
|---|---|---|
vendor_id |
INQUIRY bytes [8:16] | SPC-4 section 6.4.2 |
product_id |
INQUIRY bytes [16:32] | SPC-4 section 6.4.2 |
product_revision |
INQUIRY bytes [32:36] | SPC-4 section 6.4.2 |
vendor_specific |
INQUIRY bytes [36:43] | SPC-4 section 6.4.2 |
firmware_date |
GET CONFIGURATION Feature 010C | MMC-6 section 5.3.10 |
The match key is "VENDOR|PRODUCT|REVISION|VENDOR_SPECIFIC". Profile matching
tries all four fields first, then falls back to matching without the firmware
date for drives where Feature 010C is unavailable.
Drive Profiles
Profiles are JSON objects compiled into the binary (profiles.json).
Each profile contains:
| Field | Purpose |
|---|---|
vendor_id, product_revision, vendor_specific, firmware_date |
Matching fields |
chipset |
"mediatek" or "renesas" |
unlock_mode, unlock_buf_id |
READ BUFFER CDB parameters |
signature |
Expected 4-byte response signature |
unlock_cdb |
Pre-built unlock CDB (hex-encoded) |
register_offsets |
Offsets for hardware register reads |
capabilities |
Feature flags: bd_raw_read, dvd_all_regions, etc. |
Loading:
// Bundled (compiled-in) -- no file I/O
let profiles = profile::load_bundled()?;
// External file
let profiles = profile::load_all(Path::new("/path/to/profiles.json"))?;
Chipsets
MediaTek MT1959
Covers all LG, ASUS, and HP optical drives. Two sub-variants share identical logic with different SCSI parameters:
| Variant | READ BUFFER mode | Buffer ID |
|---|---|---|
| MT1959-A | 0x01 | 0x44 |
| MT1959-B | 0x02 | 0x77 |
The Platform trait maps to command handlers:
| Handler | Function | Description |
|---|---|---|
| 0 | unlock() |
Send READ BUFFER, verify signature + verification bytes |
| 1 | read_config() |
Read 1888-byte configuration block + 4-byte status |
| 2-3 | read_register() |
Read hardware registers at profile-specified offsets |
| 4 | calibrate() |
Probe disc surface, build 64-entry speed table |
| 5 | keepalive() |
Periodic session maintenance |
| 6 | status() |
Query current mode and feature flags |
| 7 | probe() |
Generic READ BUFFER with dynamic parameters |
| 8 | read_sectors() |
Speed lookup + SET CD SPEED + READ(10) with flag 0x08 |
| 9 | timing() |
Timing calibration |
Renesas (Planned)
RS8xxx/RS9xxx chipsets used in Pioneer and some HL-DT-ST drives.
Currently returns Error::UnsupportedDrive when a Renesas profile is matched.
Why Unlock Is Needed
Optical drive firmware restricts what applications can read from disc. Without unlock:
-
READ(10) works for unencrypted filesystem data. UDF structures, MPLS playlists, and CLPI clip info are readable without unlock. Standard READ(10) works on any drive.
-
READ(10) fails for encrypted content sectors. The drive firmware returns SCSI errors (sense key 0x05, illegal request) when an application attempts to read sectors containing encrypted m2ts content without prior AACS authentication via the bus key.
-
Raw mode bypasses firmware restrictions. After unlock, the drive accepts READ(10) with the raw read flag (CDB byte 1 = 0x08) for all sectors, regardless of encryption status.
AACS Before Unlock
AACS bus authentication uses standard MMC REPORT KEY / SEND KEY commands.
On some drives these must execute before unlock. The Disc::scan() handles
this internally — it manages the handshake/unlock ordering automatically.
Speed Control
After probe_disc(), the platform driver maintains a speed lookup table
built by probing the disc surface. On each read() call, the driver:
- Looks up the optimal speed for the target LBA.
- Issues SET CD SPEED (0xBB) if the speed differs from current.
- Performs the READ(10).
Available speeds:
| Format | Speeds |
|---|---|
| Blu-ray | 1x (4,500 KB/s) through 12x (54,000 KB/s) |
| DVD | 1x (1,385 KB/s) through 16x (22,160 KB/s) |
| Max | 0xFFFF (drive decides) |