libfreemkv 0.10.3

Open source raw disc access library for optical drives
Documentation
# 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

```rust
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)` | Read sectors with built-in error recovery |
| `reset()` | Close/reopen device, TUR, escalate if needed |
| `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() with Recovery

`Drive::read()` is the single read method. On error:

1. Set minimum speed immediately
2. Reset device (close/reopen/TUR)
3. Wait 2s for drive to settle
4. Retry at min speed, min batch (3 sectors)
5. If still failing: skip sectors, zero-fill, log
6. Stay at min speed for 500 MB after error (recovery window)

---

## SCSI Transport

### Trait

```rust
pub trait ScsiTransport: Send {
    fn execute(
        &mut self,
        cdb: &[u8],
        direction: DataDirection,
        data: &mut [u8],
        timeout_ms: u32,
    ) -> Result<ScsiResult>;

    fn reset(&mut self, device: &str) -> Result<()>;
}
```

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``ioctl(fd, SG_IO, &hdr)` | `/dev/sg*` |
| macOS | `MacScsiTransport` — IOKit SCSITask | IOKit service |
| Windows | `WindowsScsiTransport` — SPTI | `\\.\CdRomN` |

The Linux backend opens with `O_RDWR | O_NONBLOCK`, constructs `sg_io_hdr`,
and returns `ScsiResult` with status, bytes transferred, and sense data.

On non-zero SCSI status, the transport parses sense key, ASC, and ASCQ from the
sense buffer and returns `Error::ScsiError`.

### 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:

```rust
// 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) |