From 9f422e6ebb7d32afd69bb2e39699a958b4cba26e Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:10:51 -0700 Subject: [PATCH] docs: describe the generic Unlocker seam, drop in-tree firmware specifics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 18 ++--- docs/api-design.md | 5 +- docs/architecture.md | 44 ++++++------- docs/disc-to-rip.md | 12 ++-- docs/drive-access.md | 152 ++++++++++++++++++------------------------- 5 files changed, 102 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index b2ace11..dbc848f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/api-design.md b/docs/api-design.md index 70ec256..13f8832 100644 --- a/docs/api-design.md +++ b/docs/api-design.md @@ -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 diff --git a/docs/architecture.md b/docs/architecture.md index 6d89fb1..2599044 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. --- diff --git a/docs/disc-to-rip.md b/docs/disc-to-rip.md index 1ba39fb..515e9e3 100644 --- a/docs/disc-to-rip.md +++ b/docs/disc-to-rip.md @@ -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 diff --git a/docs/drive-access.md b/docs/drive-access.md index 131220f..a2e2ce2 100644 --- a/docs/drive-access.md +++ b/docs/drive-access.md @@ -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> { 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: