Files
libfreemkv/docs/drive-access.md
T
MattJackson 7cd2c937ed 0.18.1 docs: refresh README, CHANGELOG, and docs/ for the trait split
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.
2026-05-09 12:13:51 -07:00

10 KiB
Raw Blame History

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:

  1. Platform driver run_init() — sends vendor-specific SCSI commands
  2. If firmware upload needed: upload, wait 10s for drive reset, retry
  3. Speed calibration after unlock
  4. 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::patch 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 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:

  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.

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.00.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:

  1. Looks up the optimal speed for the target LBA.
  2. Issues SET CD SPEED (0xBB) if the speed differs from current.
  3. 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)