docs: describe the generic Unlocker seam, drop in-tree firmware specifics
The docs still documented the old in-tree firmware unlocker: the MediaTek MT1959 variant table, the READ BUFFER unlock CDB bytes, the profiles.json schema (unlock_mode/unlock_buf_id/unlock_cdb), the platform/mt1959 driver listings, and the 'why unlock is needed' handshake mechanism. None of that lives in libfreemkv anymore — the core is firmware-clean and ships only the pluggable Unlocker trait + registry (src/unlock.rs). Rewrite drive-access, architecture, api-design, disc-to-rip, and the README to describe only the generic Unlocker seam: the trait, register_unlocker, the registry routing, and the host-cert fallback when no unlocker matches. Point readers to the freemkv-unlock repo for concrete unlockers. No source change.
This commit is contained in:
@@ -4,11 +4,11 @@
|
||||
|
||||
# libfreemkv
|
||||
|
||||
Rust library for 4K UHD / Blu-ray / DVD optical drives. Drive access, disc scanning, stream labels, AACS decryption, CSS decryption, KEYDB updates, and content reading in one crate. Bundled drive profiles — no external files needed.
|
||||
Rust library for 4K UHD / Blu-ray / DVD optical drives. Drive access, disc scanning, stream labels, AACS decryption, CSS decryption, KEYDB updates, and content reading in one crate. Firmware-clean core: drive-unlock support is plugged in via the `Unlocker` trait, with concrete unlockers shipped as separate crates.
|
||||
|
||||
DVDs (CSS) decrypt out of the box. Blu-ray and UHD (AACS) require a `keydb.cfg` (default `~/.config/freemkv/keydb.cfg`) supplying disc-specific volume unique keys; no AACS key material is compiled in.
|
||||
|
||||
**12+ MB/s** sustained read speeds on BD. Full init: unlock, firmware upload, speed calibration — all from pure Rust.
|
||||
**12+ MB/s** sustained read speeds on BD. Drive prep routes through the pluggable unlock seam — register an unlocker and `init()` drives it; with none registered the library rips via the host-certificate AACS handshake.
|
||||
|
||||
Multi-lingual by design — the library outputs structured data and numeric error codes, never English text. Build any UI or localization on top.
|
||||
|
||||
@@ -29,10 +29,10 @@ libfreemkv = "1.0.0-rc.1"
|
||||
use libfreemkv::{Drive, Disc, ScanOptions};
|
||||
use std::path::Path;
|
||||
|
||||
// Open drive — profiles are bundled, auto-identified
|
||||
// Open drive — identified via INQUIRY
|
||||
let mut drive = Drive::open(Path::new("/dev/sg4"))?;
|
||||
drive.wait_ready()?; // wait for disc
|
||||
drive.init()?; // unlock + firmware upload
|
||||
drive.init()?; // route through the unlock seam (if an unlocker is registered)
|
||||
drive.probe_disc()?; // probe disc surface for optimal speeds
|
||||
|
||||
// Scan disc — UDF, playlists, streams, AACS (all automatic)
|
||||
@@ -100,7 +100,7 @@ loop {
|
||||
|
||||
## What It Does
|
||||
|
||||
- **Drive access** — open, identify, unlock, firmware upload, speed calibration, eject
|
||||
- **Drive access** — open, identify, pluggable unlock seam, speed control, eject
|
||||
- **12+ MB/s reads** — auto-detects kernel transfer limits, sustained full speed
|
||||
- **Disc scanning** — UDF 2.50 filesystem, MPLS playlists, CLPI clip info
|
||||
- **Stream labels** — 5 BD-J format parsers (Paramount, Criterion, Pixelogic, CTRM, Deluxe)
|
||||
@@ -132,10 +132,10 @@ Blu-rays and UHD (AACS) require a `keydb.cfg` at `~/.config/freemkv/keydb.cfg` (
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
Drive — open, identify, init, unlock, single-shot read
|
||||
Drive — open, identify, init, single-shot read
|
||||
├── ScsiTransport — SG_IO (Linux), IOKit (macOS), SPTI (Windows)
|
||||
├── DriveProfile — per-drive unlock parameters (bundled)
|
||||
└── PlatformDriver — MediaTek (supported), Renesas (planned)
|
||||
└── Unlocker seam — pluggable trait + registry; concrete unlockers
|
||||
live in the separate freemkv-unlock repo
|
||||
|
||||
Disc — scan titles, streams, AACS/CSS state
|
||||
├── UDF reader — Blu-ray UDF 2.50 with metadata partitions
|
||||
@@ -186,7 +186,7 @@ All errors are structured with numeric codes. No user-facing English text — ap
|
||||
|
||||
## Contributing
|
||||
|
||||
Run `freemkv info disc:// --share` with the [freemkv CLI](https://github.com/freemkv/freemkv) to contribute your drive's profile.
|
||||
Run `freemkv info disc:// --share` with the [freemkv CLI](https://github.com/freemkv/freemkv) to capture your drive's identity for contribution. Drive-unlock profiles are maintained in the [freemkv-unlock](https://github.com/freemkv/freemkv-unlock) repository.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+2
-3
@@ -171,7 +171,7 @@ libfreemkv/src/
|
||||
│ └── writeback.rs sync_file_range pipeline
|
||||
├── drive/ Drive (open, init, single-shot read)
|
||||
│ ├── mod.rs Drive struct, init, read (single-shot), reset, eject
|
||||
│ ├── capture.rs Drive profile capture for contribution
|
||||
│ ├── capture.rs Raw drive SCSI capture (INQUIRY/GET_CONFIG) for contribution
|
||||
│ ├── linux.rs Linux drive discovery
|
||||
│ ├── macos.rs macOS drive discovery
|
||||
│ └── windows.rs Windows drive discovery
|
||||
@@ -182,7 +182,7 @@ libfreemkv/src/
|
||||
│ ├── mapfile.rs ddrescue-format mapfile
|
||||
│ └── read_error.rs ReadCtx / ReadAction state machine
|
||||
├── scsi/ SCSI transport (Linux SG_IO, macOS IOKit, Windows SPTI)
|
||||
├── platform/ Drive unlock (MT1959 A/B)
|
||||
├── unlock.rs Unlocker trait + registry (pluggable unlock seam)
|
||||
├── aacs/ AACS decryption (handshake, keys, keydb, decrypt)
|
||||
├── css/ DVD CSS cipher
|
||||
├── decrypt.rs Unified decrypt dispatcher (AACS/CSS/None)
|
||||
@@ -198,7 +198,6 @@ libfreemkv/src/
|
||||
├── labels/ BD-J label extraction (5 format parsers)
|
||||
├── keydb.rs KEYDB download, parse, save
|
||||
├── identity.rs DriveId from INQUIRY
|
||||
├── profile.rs Bundled drive profiles
|
||||
├── speed.rs DriveSpeed enum
|
||||
├── mux/
|
||||
│ ├── mod.rs Public mux exports
|
||||
|
||||
+22
-22
@@ -17,9 +17,10 @@ material is compiled in; DVD CSS player keys are the only compiled-in keys.
|
||||
format handling live in the library. CLI binaries are thin wrappers that call
|
||||
`Drive::open()` and `Disc::scan()`.
|
||||
|
||||
2. **No external files.** Bundled drive profiles are compiled into the binary via
|
||||
`include_str!`. No configuration directory, no runtime file lookups for drive
|
||||
support.
|
||||
2. **Firmware-clean core.** libfreemkv ships no firmware, no unlock CDBs, and no
|
||||
drive profiles. Drive-unlock logic is plugged in by an external crate through
|
||||
the `Unlocker` trait + registry (`register_unlocker`); without one the library
|
||||
still rips via the host-certificate AACS handshake.
|
||||
|
||||
3. **Transparent AACS.** The `ContentReader` decrypts on the fly when keys are
|
||||
available. Callers read cleartext sectors without knowing whether the disc
|
||||
@@ -43,11 +44,9 @@ material is compiled in; DVD CSS player keys are the only compiled-in keys.
|
||||
libfreemkv (lib.rs)
|
||||
│
|
||||
├── Drive Access
|
||||
│ ├── drive Drive — open, identify, init, unlock, single-shot read
|
||||
│ ├── drive Drive — open, identify, init, single-shot read
|
||||
│ ├── scsi ScsiTransport trait + platform backends (sg async, IOKit, SPTI)
|
||||
│ ├── platform/ Platform trait — per-chipset command handlers
|
||||
│ │ └── mt1959 MediaTek MT1959 driver (LG, ASUS, HP)
|
||||
│ ├── profile DriveProfile loading, matching, bundled JSON
|
||||
│ ├── unlock Unlocker trait + registry — the pluggable unlock seam
|
||||
│ ├── identity DriveId from INQUIRY + GET_CONFIG 010C
|
||||
│ ├── speed DriveSpeed enum, SET CD SPEED CDB builder
|
||||
│ └── event Event system for drive status callbacks
|
||||
@@ -76,8 +75,7 @@ libfreemkv (lib.rs)
|
||||
│
|
||||
├── Support
|
||||
│ ├── keydb KEYDB.cfg download, parse, verify, save
|
||||
│ ├── error Error enum with numeric codes E1000-E8000
|
||||
│ └── profile Bundled drive profiles
|
||||
│ └── error Error enum with numeric codes E1000-E8000
|
||||
│
|
||||
└── lib.rs Public API re-exports
|
||||
```
|
||||
@@ -91,13 +89,12 @@ Drive::open(Path::new("/dev/sg4"))
|
||||
│
|
||||
├─ scsi::open() Open /dev/sg4 (async write/poll/read)
|
||||
├─ DriveId::from_drive() INQUIRY + GET_CONFIG 010C
|
||||
├─ profile::find_by_drive_id() Match against bundled profiles
|
||||
├─ Platform::new() Instantiate chipset driver (Mt1959)
|
||||
└─ Drive ready for init/unlock/read
|
||||
└─ Drive ready for init/read
|
||||
```
|
||||
|
||||
After open:
|
||||
- `init()` -- unlock + firmware upload + speed calibration
|
||||
- `init()` -- routes to the matching registered unlocker (if any); otherwise
|
||||
a no-op and the cert handshake carries the disc
|
||||
- `probe_disc()` -- probe disc surface for optimal speeds
|
||||
- `read(lba, count, buf, recovery)` -- single-shot read; `recovery` only selects the per-CDB timeout (1.5 s vs. 30 s)
|
||||
- `wait_ready()` -- wait for disc insertion
|
||||
@@ -197,17 +194,20 @@ implementing `execute()` for that OS and wiring it into `scsi::open()`.
|
||||
|
||||
---
|
||||
|
||||
## Chipset Support
|
||||
## Drive Unlock
|
||||
|
||||
| Chipset | Drives | Status |
|
||||
|---------|--------|--------|
|
||||
| MediaTek MT1959 | LG, ASUS, HP | Supported (bundled profiles) |
|
||||
| Renesas RS8xxx/RS9xxx | Pioneer, some HL-DT-ST | Planned |
|
||||
libfreemkv carries no drive-unlock mechanism. The `Unlocker` trait + registry
|
||||
(`src/unlock.rs`) is the seam: an external crate implements `Unlocker` and
|
||||
registers it once via `register_unlocker(...)`. At drive-prep the registry is
|
||||
walked in order and the first unlocker whose `matches()` is true is asked to
|
||||
`unlock_drive()` over the raw `ScsiTransport`. If none match, the drive is left
|
||||
untouched and the host-certificate AACS handshake carries the disc.
|
||||
|
||||
The `Platform` trait abstracts chipset-specific commands. Each chipset implements
|
||||
handlers (unlock, config, register, calibrate, keepalive, status, probe,
|
||||
read_sectors, timing). All handlers are accessed via SCSI READ BUFFER with
|
||||
chipset-specific mode and buffer ID bytes.
|
||||
The implementor owns everything firmware-specific — drive profiles, vendor CDBs,
|
||||
variant logic. Concrete unlockers live in the separate
|
||||
**[freemkv-unlock](https://github.com/freemkv/freemkv-unlock)** repository, never
|
||||
in libfreemkv. See [`drive-access.md`](drive-access.md#drive-unlock-seam) for the
|
||||
trait definition and routing.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+6
-6
@@ -10,14 +10,14 @@ Insert disc
|
||||
│
|
||||
▼
|
||||
1. Open drive (drive/mod.rs)
|
||||
│ INQUIRY → identify drive
|
||||
│ Match bundled profile → chipset, unlock parameters
|
||||
│ INQUIRY → identify drive (DriveId)
|
||||
│
|
||||
▼
|
||||
2. Init drive (drive/mod.rs → platform/mt1959)
|
||||
│ Firmware upload (if needed, 10s recovery wait)
|
||||
│ Unlock → vendor-specific command activates raw read mode
|
||||
│ Speed calibration → probe_disc()
|
||||
2. Init drive (drive/mod.rs → unlock seam)
|
||||
│ Walk the registered-unlocker registry; first match unlocks the drive
|
||||
│ (firmware/vendor handshakes are the unlocker's own business)
|
||||
│ No match → drive untouched; host-cert AACS handshake carries the disc
|
||||
│ Speed control → probe_disc()
|
||||
│
|
||||
▼
|
||||
3. AACS handshake (aacs/handshake.rs) — optional
|
||||
|
||||
+63
-89
@@ -7,8 +7,9 @@ optical drives.
|
||||
|
||||
## Drive
|
||||
|
||||
`Drive` is the primary API. It owns the SCSI transport, the matched
|
||||
drive profile, and the chipset-specific platform driver.
|
||||
`Drive` is the primary API. It owns the SCSI transport and the drive
|
||||
identity (`DriveId`); any drive-specific unlock logic lives behind the
|
||||
pluggable [unlock seam](#drive-unlock-seam), not in `Drive` itself.
|
||||
|
||||
### Opening a Drive
|
||||
|
||||
@@ -16,15 +17,16 @@ drive profile, and the chipset-specific platform driver.
|
||||
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()`.
|
||||
`open()` performs: open device → send INQUIRY → build `DriveId`. The drive
|
||||
is ready for `wait_ready()` and `init()` (which routes through the unlock
|
||||
seam).
|
||||
|
||||
### Drive Operations
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `wait_ready()` | Wait for disc insertion (30s timeout, TUR polling) |
|
||||
| `init()` | Firmware upload + unlock + speed calibration |
|
||||
| `init()` | Route to the matching registered unlocker (if any), then prepare for reads |
|
||||
| `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. |
|
||||
@@ -32,17 +34,21 @@ platform driver. The drive is ready for `wait_ready()` and `init()`.
|
||||
| `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 |
|
||||
| `has_profile()` | Whether a registered unlocker matches this drive |
|
||||
| `close()` | Consume Drive, cleanup (also runs via Drop) |
|
||||
|
||||
### init() Sequence
|
||||
|
||||
`init()` orchestrates the full drive unlock:
|
||||
`init()` routes drive preparation through the unlock seam:
|
||||
|
||||
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
|
||||
1. Walk the registered-unlocker registry; the first whose `matches()` is true
|
||||
is asked to `unlock_drive()` over the raw transport.
|
||||
2. Whatever that unlocker needs (firmware upload, vendor handshakes, retries)
|
||||
is the unlocker's own business — libfreemkv only forwards the transport.
|
||||
3. If no unlocker matches, the drive is left untouched and the library uses
|
||||
the host-certificate AACS handshake.
|
||||
|
||||
See [Drive Unlock Seam](#drive-unlock-seam) for the trait and registry.
|
||||
|
||||
### read() — single-shot
|
||||
|
||||
@@ -164,100 +170,68 @@ date for drives where Feature 010C is unavailable.
|
||||
|
||||
---
|
||||
|
||||
## Drive Profiles
|
||||
## Drive Unlock Seam
|
||||
|
||||
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:
|
||||
libfreemkv ships **no firmware, no unlock CDBs, and no drive profiles.** It
|
||||
knows only the *seam*, never the *mechanism*. The seam is the `Unlocker`
|
||||
trait plus a small process-wide registry (`src/unlock.rs`):
|
||||
|
||||
```rust
|
||||
// Bundled (compiled-in) -- no file I/O
|
||||
let profiles = profile::load_bundled()?;
|
||||
pub trait Unlocker: Send + Sync {
|
||||
/// Stable, language-neutral identifier (logged).
|
||||
fn name(&self) -> &str;
|
||||
|
||||
// External file
|
||||
let profiles = profile::load_all(Path::new("/path/to/profiles.json"))?;
|
||||
/// True if this unlocker handles the given drive.
|
||||
fn matches(&self, id: &DriveId) -> bool;
|
||||
|
||||
/// Put the drive into extended-access mode. The one required capability.
|
||||
fn unlock_drive(&self, scsi: &mut dyn ScsiTransport, id: &DriveId) -> Result<()>;
|
||||
|
||||
/// Read the disc Volume ID via the drive's OEM path. Default: no-op.
|
||||
fn read_volume_id(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId)
|
||||
-> Result<Option<[u8; 16]>> { Ok(None) }
|
||||
|
||||
/// Raise the drive to its maximum read speed. Default: no-op.
|
||||
fn set_max_read_speed(&self, _scsi: &mut dyn ScsiTransport, _id: &DriveId)
|
||||
-> Result<()> { Ok(()) }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
An unlocker is supplied by an **external crate** and registered once at
|
||||
process start:
|
||||
|
||||
## Chipsets
|
||||
```rust
|
||||
libfreemkv::register_unlocker(Box::new(some_unlocker::Plugin::new()));
|
||||
```
|
||||
|
||||
### MediaTek MT1959
|
||||
The implementor owns everything about *how* a particular drive family is
|
||||
driven — drive identification against its own profile database, firmware
|
||||
upload, vendor CDBs, variant logic. libfreemkv only hands over the raw
|
||||
`ScsiTransport` and the `DriveId`.
|
||||
|
||||
Covers all LG, ASUS, and HP optical drives. Two sub-variants share identical
|
||||
logic with different SCSI parameters:
|
||||
### Routing
|
||||
|
||||
| Variant | READ BUFFER mode | Buffer ID |
|
||||
|---------|------------------|-----------|
|
||||
| MT1959-A | 0x01 | 0x44 |
|
||||
| MT1959-B | 0x02 | 0x77 |
|
||||
At drive-prep the registry is walked in registration order; the first
|
||||
unlocker whose `matches()` returns true is asked to `unlock_drive()` (and,
|
||||
when needed, `read_volume_id()` / `set_max_read_speed()`). If no unlocker
|
||||
matches, the drive is left untouched and the library falls back to the
|
||||
standard host-certificate AACS handshake (the "OEM route"). The
|
||||
`register_unlocker(...)` line is the entire plug: drop it (and the unlocker
|
||||
crate) and libfreemkv still compiles and rips via the cert handshake.
|
||||
|
||||
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.
|
||||
Concrete unlockers — including the firmware-unlock profile databases,
|
||||
variant logic, and vendor CDBs that used to live in-tree — are maintained
|
||||
in the separate **[freemkv-unlock](https://github.com/freemkv/freemkv-unlock)**
|
||||
repository, never here.
|
||||
|
||||
---
|
||||
|
||||
## 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).
|
||||
A matching unlocker may raise the drive to its maximum read speed via
|
||||
`set_max_read_speed()` (a no-op when no unlocker matches or the unlocker
|
||||
declines). The library issues SET CD SPEED (0xBB) through the generic CDB
|
||||
builder; the concrete speed policy lives in the unlocker.
|
||||
|
||||
Available speeds:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user