libfreemkv v0.1.0 — Open source 4K UHD / Blu-ray / DVD drive library

Features:
- Open drive identification via SPC-4 INQUIRY + MMC-6 GET CONFIGURATION
- 141 supported drives with bundled profiles
- MT1959 platform: unlock, calibrate, raw sector reads
- DriveSpeed enum: BD1x-BD12x, DVD1x-DVD16x
- Field names follow SPC-4 §6.4.2 and MMC-6 §5.3.10 standards
- No proprietary fingerprints — open matching by SCSI fields
- Zero config: profiles compiled into binary

Tested on real hardware: HL-DT-ST BD-RE BU40N 1.03
This commit is contained in:
MattJackson
2026-04-06 10:00:00 -07:00
commit b9ea1d29dd
17 changed files with 3781 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
/target
Cargo.lock
*.swp
*.swo
.DS_Store
+68
View File
@@ -0,0 +1,68 @@
# Contributing to libfreemkv
Thank you for your interest in helping make disc archival accessible to everyone.
## Contributing Drive Profiles
The most impactful contribution is adding support for new drives. If you have
an optical drive that isn't listed in [profiles/](profiles/), we'd love your help.
### How to submit drive data
1. Install the tool:
```bash
cargo install libfreemkv
```
2. Run `freemkv-info` with your drive:
```bash
freemkv-info /dev/sr0 --raw > my_drive.txt
```
3. Open a pull request or issue with the output file attached.
That's it. The raw SCSI response data lets us build a profile for your drive.
### What data is collected
`freemkv-info --raw` sends two standard SCSI commands to your drive:
- **INQUIRY** (opcode 0x12) — returns drive vendor, model, firmware version
- **GET CONFIGURATION** (opcode 0x46) — returns drive feature data
These are read-only, standard SCSI commands. They don't modify your drive
or access any disc data. Every operating system sends these commands
automatically when a drive is connected.
### Priority: Pioneer drives
We especially need data from **Pioneer** Blu-ray drives (BDR-S08, BDR-S09,
BDR-S12, BDR-S13, BDR-209, BDR-212, etc). If you have one, your contribution
would help unlock support for 130+ Pioneer drive firmware versions.
## Contributing Code
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/my-change`)
3. Write tests for your changes
4. Ensure `cargo test` and `cargo clippy` pass
5. Submit a pull request
### Code Style
- Run `cargo fmt` before committing
- No `unsafe` without a comment explaining why
- Public APIs need doc comments
- Error handling via `Result<T, Error>`, no panics in library code
### Architecture
- `src/scsi.rs` — SCSI transport layer (SG_IO on Linux)
- `src/profile.rs` — Profile loading and matching
- `src/platform/` — Per-chipset command implementations
- `src/drive.rs` — High-level DriveSession API
- `profiles/` — JSON drive profile data
## License
By contributing, you agree that your contributions will be licensed under AGPL-3.0.
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "libfreemkv"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-only"
description = "Open source raw disc access library for optical drives"
repository = "https://github.com/freemkv/libfreemkv"
keywords = ["bluray", "uhd", "optical", "scsi", "disc"]
categories = ["hardware-support", "multimedia"]
[dependencies]
libc = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha1 = "0.10"
aes = "0.8"
cbc = "0.1"
flate2 = "1"
[[bin]]
name = "freemkv-info"
path = "src/bin/freemkv_info.rs"
[[bin]]
name = "freemkv-test"
path = "src/bin/freemkv_test.rs"
+16
View File
@@ -0,0 +1,16 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2026 FreeMKV Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
+125
View File
@@ -0,0 +1,125 @@
# libfreemkv
Open source raw disc access library for UHD Blu-ray optical drives.
Enables direct sector reading on compatible drives for UHD Blu-ray archival,
backup, and media extraction. Ships with community-contributed drive profiles —
no proprietary data files needed at runtime.
## Features
- **Drive identification** — SCSI INQUIRY + GET CONFIGURATION for automatic profile matching
- **Raw read mode** — activate enhanced read mode on supported drives
- **Speed calibration** — optimal read speed per disc region
- **Raw sector reading** — direct READ(10) access to disc sectors
- **Drive profiles** — per-drive SCSI command data, shipped as JSON files
- **Community-driven** — submit new drive profiles via `freemkv-info`
## Supported Drives
Currently supports 280+ LG, ASUS, and HP optical drive firmware versions
across the MediaTek MT1959 chipset family. Pioneer Renesas support is in progress.
See [profiles/](profiles/) for the full list.
## Installation
```bash
cargo install libfreemkv
```
Or add to your `Cargo.toml`:
```toml
[dependencies]
libfreemkv = "0.1"
```
## Quick Start
### As a library
```rust
use libfreemkv::DriveSession;
use std::path::Path;
let mut session = DriveSession::open(
Path::new("/dev/sr0"),
Path::new("profiles/"),
)?;
session.enable()?; // activate raw read mode
session.calibrate()?; // optimize read speed
let mut buf = vec![0u8; 2048];
session.read_sectors(0, 1, &mut buf)?;
```
### freemkv-info
Identify your drive and check compatibility:
```bash
$ freemkv-info /dev/sr0
Drive: HL-DT-ST BD-RE BU40N 1.03
Chipset: MT1959
Raw Read: Supported
Profile: Found (mt1959_a)
$ freemkv-info /dev/sr0 --raw
# Dumps full INQUIRY and GET CONFIGURATION responses as hex
# Useful for contributing profiles for unsupported drives
```
### freemkv-test
Verify raw read mode works:
```bash
$ freemkv-test /dev/sr0
Enabling raw read mode... OK
Calibrating speed... OK (42 speed zones)
Reading sector 0... OK (2048 bytes)
Reading sector 1000... OK (2048 bytes)
All checks passed.
```
## Contributing Drive Profiles
If your drive isn't supported, you can help:
1. Run `freemkv-info /dev/sr0 --raw > my_drive.txt`
2. Open an issue or PR with the output
3. We'll generate a profile from your drive data
This is especially needed for Pioneer drives.
## Architecture
```
DriveSession
├── ScsiTransport — SG_IO (Linux) / IOKit (macOS)
├── DriveProfile — per-drive JSON data
└── Platform — per-chipset unlock + read logic
├── Mt1959 — LG/ASUS MediaTek drives
└── Pioneer — Pioneer Renesas drives (WIP)
```
The library implements 10 drive commands per platform:
| Command | Purpose |
|---------|---------|
| enable | Activate raw read mode |
| read_config | Read drive configuration |
| read_register | Read hardware registers |
| calibrate | Build speed optimization table |
| keepalive | Session keepalive |
| status | Read mode status and features |
| probe | Generic drive query |
| read_sectors | Read raw disc sectors |
| read_disc_structure | Read disc metadata |
| timing | Timing calibration |
## License
AGPL-3.0-only
+1836
View File
File diff suppressed because it is too large Load Diff
+159
View File
@@ -0,0 +1,159 @@
//! freemkv-info — Drive identification and compatibility checker.
//!
//! Sends standard SCSI INQUIRY and GET CONFIGURATION commands to an optical drive,
//! displays drive identity and compatibility status, and optionally outputs raw
//! response data for profile contribution.
//!
//! Usage:
//! freemkv-info /dev/sr0
//! freemkv-info /dev/sr0 --raw
//! freemkv-info /dev/sr0 --json
use std::env;
use std::path::Path;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("freemkv-info — Drive identification and compatibility checker");
eprintln!();
eprintln!("Usage: freemkv-info <device> [options]");
eprintln!();
eprintln!(" <device> Optical drive device (e.g. /dev/sr0)");
eprintln!(" --raw Output raw SCSI response hex (for profile contribution)");
eprintln!(" --json Output machine-readable JSON");
eprintln!(" --profiles Path to profiles directory (default: ./profiles)");
eprintln!();
eprintln!("Examples:");
eprintln!(" freemkv-info /dev/sr0");
eprintln!(" freemkv-info /dev/sr0 --raw > my_drive.txt");
process::exit(1);
}
let device = Path::new(&args[1]);
let raw_mode = args.iter().any(|a| a == "--raw");
let json_mode = args.iter().any(|a| a == "--json");
let profiles_dir = args.iter()
.position(|a| a == "--profiles")
.and_then(|i| args.get(i + 1))
.map(|s| s.as_str())
.unwrap_or("profiles");
// Open SCSI transport
let mut transport = match libfreemkv::scsi::SgIoTransport::open(device) {
Ok(t) => t,
Err(e) => {
eprintln!("Error: Cannot open {}: {}", device.display(), e);
process::exit(1);
}
};
// INQUIRY
let inquiry = match libfreemkv::scsi::inquiry(&mut transport) {
Ok(i) => i,
Err(e) => {
eprintln!("Error: INQUIRY failed: {}", e);
process::exit(1);
}
};
// GET CONFIGURATION feature 0x010C
let gc_010c = libfreemkv::scsi::get_config_010c(&mut transport).ok();
if json_mode {
print_json(&inquiry, &gc_010c);
} else if raw_mode {
print_raw(&inquiry, &gc_010c);
} else {
print_human(&inquiry, &gc_010c, profiles_dir);
}
}
fn print_human(
inquiry: &libfreemkv::scsi::InquiryResult,
gc_010c: &Option<Vec<u8>>,
profiles_dir: &str,
) {
println!("freemkv-info v{}", env!("CARGO_PKG_VERSION"));
println!();
println!("Drive: {} {} {}", inquiry.vendor_id, inquiry.model, inquiry.firmware);
println!("INQUIRY: additional_length=0x{:02X} ({})",
inquiry.raw.get(4).unwrap_or(&0),
inquiry.raw.get(4).unwrap_or(&0));
if let Some(gc) = gc_010c {
let data_hex: String = gc.iter().map(|b| format!("{:02x}", b)).collect();
println!("Feature 0x010C: {}", data_hex);
} else {
println!("Feature 0x010C: not available");
}
// Try to match profile
if let Ok(profiles) = libfreemkv::profile::load_all(Path::new(profiles_dir)) {
let matched = profiles.iter().find(|p| {
p.drive_id.contains(&inquiry.vendor_id)
&& p.drive_id.contains(&inquiry.model)
});
println!();
match matched {
Some(p) => {
println!("Profile: FOUND ({})", p.platform.name());
println!("Raw Read: Supported");
}
None => {
println!("Profile: NOT FOUND");
println!("Raw Read: Unknown — run with --raw and submit a profile request");
}
}
} else {
println!();
println!("Profile: No profiles directory found at '{}'", profiles_dir);
}
}
fn print_raw(
inquiry: &libfreemkv::scsi::InquiryResult,
gc_010c: &Option<Vec<u8>>,
) {
println!("# freemkv-info raw output");
println!("# Submit this file to https://github.com/freemkv/libfreemkv/issues");
println!();
println!("vendor: {}", inquiry.vendor_id);
println!("model: {}", inquiry.model);
println!("firmware: {}", inquiry.firmware);
println!();
// Full INQUIRY hex
println!("inquiry_hex: {}", hex_encode(&inquiry.raw));
println!("inquiry_length: {}", inquiry.raw.len());
// GET CONFIG 0x010C
if let Some(gc) = gc_010c {
println!("get_config_010c_hex: {}", hex_encode(gc));
println!("get_config_010c_length: {}", gc.len());
} else {
println!("get_config_010c_hex: ERROR");
}
}
fn print_json(
inquiry: &libfreemkv::scsi::InquiryResult,
gc_010c: &Option<Vec<u8>>,
) {
let json = serde_json::json!({
"vendor": inquiry.vendor_id,
"model": inquiry.model,
"firmware": inquiry.firmware,
"inquiry_hex": hex_encode(&inquiry.raw),
"inquiry_length": inquiry.raw.len(),
"get_config_010c_hex": gc_010c.as_ref().map(|g| hex_encode(g)),
});
println!("{}", serde_json::to_string_pretty(&json).unwrap());
}
fn hex_encode(data: &[u8]) -> String {
data.iter().map(|b| format!("{:02x}", b)).collect()
}
+99
View File
@@ -0,0 +1,99 @@
//! freemkv-test — Quick verification that raw disc access works.
//!
//! Enables raw read mode, calibrates speed, reads a few test sectors.
//! Use this to verify your drive and profile are working correctly.
//!
//! Usage:
//! freemkv-test /dev/sr0
//! freemkv-test /dev/sr0 --profiles ./profiles
use std::env;
use std::path::Path;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("freemkv-test — Verify raw disc access works");
eprintln!();
eprintln!("Usage: freemkv-test <device> [--profiles <dir>]");
process::exit(1);
}
let device = Path::new(&args[1]);
println!("freemkv-test v{}", env!("CARGO_PKG_VERSION"));
println!();
// Open drive session (uses bundled profiles)
print!("Opening {}... ", device.display());
let mut session = match libfreemkv::DriveSession::open(device) {
Ok(s) => { println!("OK"); s }
Err(e) => { println!("FAILED: {}", e); process::exit(1); }
};
println!(" Drive ID: {}", session.profile.drive_id);
println!(" Platform: {}", session.profile.platform.name());
println!();
// Enable raw read mode
print!("Unlocking drive... ");
match session.unlock() {
Ok(()) => println!("OK"),
Err(e) => { println!("FAILED: {}", e); process::exit(1); }
}
// Check status
print!("Checking status... ");
match session.status() {
Ok(status) => {
if status.unlocked {
println!("OK (active)");
} else {
println!("WARNING: drive reported as locked");
}
}
Err(e) => println!("SKIP ({})", e),
}
// Calibrate speed
print!("Calibrating speed... ");
match session.calibrate() {
Ok(()) => println!("OK"),
Err(e) => println!("SKIP ({})", e),
}
// Read test sectors
let test_lbas: &[u32] = &[0, 100, 1000, 10000];
let mut buf = vec![0u8; 2048];
let mut pass = 0;
let mut fail = 0;
for &lba in test_lbas {
print!("Reading sector {}... ", lba);
match session.read_sectors(lba, 1, &mut buf) {
Ok(n) if n == 2048 => {
let nonzero = buf.iter().filter(|&&b| b != 0).count();
println!("OK ({} bytes, {} non-zero)", n, nonzero);
pass += 1;
}
Ok(n) => {
println!("PARTIAL ({} bytes)", n);
fail += 1;
}
Err(e) => {
println!("FAILED: {}", e);
fail += 1;
}
}
}
println!();
if fail == 0 {
println!("All {} checks passed. Drive is fully functional.", pass);
} else {
println!("{} passed, {} failed.", pass, fail);
process::exit(1);
}
}
+125
View File
@@ -0,0 +1,125 @@
//! High-level drive session — the main API for consumers.
//!
//! Opens a drive, identifies it via standard SCSI commands,
//! matches it against the profile database, and provides
//! raw disc access methods.
use std::path::Path;
use crate::error::{Error, Result};
use crate::scsi::{SgIoTransport, ScsiTransport};
use crate::identity::DriveId;
use crate::profile::{self, DriveProfile, PlatformType};
use crate::platform::{Platform, DriveStatus};
use crate::platform::mt1959::Mt1959;
/// A complete drive session.
///
/// Handles: identify → match profile → create platform → execute commands.
pub struct DriveSession {
scsi: Box<dyn ScsiTransport>,
platform: Box<dyn Platform>,
pub profile: DriveProfile,
pub drive_id: DriveId,
}
impl DriveSession {
/// Open a drive, identify it, and find the matching profile.
/// Uses the bundled profile database — no external files needed.
pub fn open(device: &Path) -> Result<Self> {
let mut transport = SgIoTransport::open(device)?;
let profiles = profile::load_bundled()?;
// Identify drive via standard SCSI commands
// SPC-4 §6.4 (INQUIRY) + MMC-6 §5.3.10 (Feature 010Ch)
let drive_id = DriveId::from_drive(&mut transport)?;
// Match drive to a profile by INQUIRY fields
let profile = profile::find_by_drive_id(&profiles, &drive_id)
.cloned()
.ok_or_else(|| Error::UnsupportedDrive(format!("{}", drive_id)))?;
if !profile.supported {
return Err(Error::UnsupportedDrive(format!(
"{} — status: {:?}", drive_id, profile.status
)));
}
let platform: Box<dyn Platform> = match profile.platform {
PlatformType::Mt1959A | PlatformType::Mt1959B => {
Box::new(Mt1959::new(profile.clone()))
}
PlatformType::Pioneer => {
return Err(Error::UnsupportedDrive("Pioneer not yet implemented".into()));
}
};
Ok(DriveSession {
scsi: Box::new(transport),
platform,
profile,
drive_id,
})
}
/// Open with an explicit profile (skip auto-detection).
pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result<Self> {
let mut transport = SgIoTransport::open(device)?;
let drive_id = DriveId::from_drive(&mut transport)?;
let platform: Box<dyn Platform> = match profile.platform {
PlatformType::Mt1959A | PlatformType::Mt1959B => {
Box::new(Mt1959::new(profile.clone()))
}
PlatformType::Pioneer => {
return Err(Error::UnsupportedDrive("Pioneer not yet implemented".into()));
}
};
Ok(DriveSession {
scsi: Box::new(transport),
platform,
profile,
drive_id,
})
}
/// Activate raw disc access mode.
pub fn unlock(&mut self) -> Result<()> {
self.platform.unlock(self.scsi.as_mut())
}
/// Check if raw disc access mode is enabled.
pub fn is_unlocked(&self) -> bool {
self.platform.is_unlocked()
}
/// Read drive status and feature flags.
pub fn status(&mut self) -> Result<DriveStatus> {
self.platform.status(self.scsi.as_mut())
}
/// Read drive configuration block.
pub fn read_config(&mut self) -> Result<Vec<u8>> {
self.platform.read_config(self.scsi.as_mut())
}
/// Read hardware register.
pub fn read_register(&mut self, index: u8) -> Result<[u8; 16]> {
self.platform.read_register(self.scsi.as_mut(), index)
}
/// Calibrate read speed for the current disc.
pub fn calibrate(&mut self) -> Result<()> {
self.platform.calibrate(self.scsi.as_mut())
}
/// Read raw disc sectors.
pub fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
self.platform.read_sectors(self.scsi.as_mut(), lba, count, buf)
}
/// Generic probe command.
pub fn probe(&mut self, sub_cmd: u8, address: u32, length: u32) -> Result<Vec<u8>> {
self.platform.probe(self.scsi.as_mut(), sub_cmd, address, length)
}
}
+46
View File
@@ -0,0 +1,46 @@
use std::fmt;
#[derive(Debug)]
pub enum Error {
DeviceNotFound(String),
UnsupportedDrive(String),
ScsiError { cdb: Vec<u8>, status: u8, sense: Vec<u8> },
UnlockFailed(String),
NotUnlocked,
NotCalibrated,
ProfileNotFound(String),
ProfileParse(String),
SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
Io(std::io::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::DeviceNotFound(s) => write!(f, "device not found: {s}"),
Error::UnsupportedDrive(s) => write!(f, "unsupported drive: {s}"),
Error::ScsiError { status, .. } => write!(f, "SCSI error: status 0x{status:02x}"),
Error::UnlockFailed(s) => write!(f, "unlock failed: {s}"),
Error::NotUnlocked => write!(f, "drive not unlocked, call unlock() first"),
Error::NotCalibrated => write!(f, "speed not calibrated, call calibrate() first"),
Error::ProfileNotFound(s) => write!(f, "no profile for: {s}"),
Error::ProfileParse(s) => write!(f, "profile parse error: {s}"),
Error::SignatureMismatch { expected, got } => {
write!(f, "signature mismatch: expected {:02x}{:02x}{:02x}{:02x}, got {:02x}{:02x}{:02x}{:02x}",
expected[0], expected[1], expected[2], expected[3],
got[0], got[1], got[2], got[3])
}
Error::Io(e) => write!(f, "I/O error: {e}"),
}
}
}
impl std::error::Error for Error {}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
pub type Result<T> = std::result::Result<T, Error>;
+152
View File
@@ -0,0 +1,152 @@
//! Drive identification — match drives to profiles by SCSI response fields.
//!
//! Field names follow SPC-4 (INQUIRY) and MMC-6 (GET CONFIGURATION) standards.
//! No proprietary fingerprints or encrypted lookups — open matching only.
//!
//! References:
//! SPC-4 §6.4.2 — Standard INQUIRY data
//! MMC-6 §5.3.10 — Feature 010Ch (Firmware Information)
use crate::error::Result;
use crate::scsi::{ScsiTransport, DataDirection};
/// Drive identity from standard SCSI commands.
///
/// All field names follow the SCSI standards:
/// - SPC-4 §6.4.2 for INQUIRY fields
/// - MMC-6 §5.3.10 for Firmware Information
#[derive(Debug, Clone)]
pub struct DriveId {
/// T10 VENDOR IDENTIFICATION — INQUIRY bytes [8:16]
/// SPC-4 §6.4.2
pub vendor_id: String,
/// PRODUCT IDENTIFICATION — INQUIRY bytes [16:32]
/// SPC-4 §6.4.2
pub product_id: String,
/// PRODUCT REVISION LEVEL — INQUIRY bytes [32:36]
/// SPC-4 §6.4.2
pub product_revision: String,
/// VENDOR SPECIFIC — INQUIRY bytes [36:43]
/// SPC-4 §6.4.2
/// Content varies by vendor: firmware type code (MTK), date (Pioneer), etc.
pub vendor_specific: String,
/// Firmware Creation Date — GET CONFIGURATION Feature 010Ch
/// MMC-6 §5.3.10
/// Format: CCYYMMDDHHMI (12 ASCII characters)
pub firmware_date: String,
/// Raw 96-byte INQUIRY response for additional parsing if needed.
pub raw_inquiry: Vec<u8>,
}
impl DriveId {
/// Probe a real drive via SCSI and build its identity.
pub fn from_drive(transport: &mut dyn ScsiTransport) -> Result<Self> {
// INQUIRY — SPC-4 §6.4
let mut inquiry = vec![0u8; 96];
let cdb_inq = [0x12, 0x00, 0x00, 0x00, 0x60, 0x00];
transport.execute(&cdb_inq, DataDirection::FromDevice, &mut inquiry, 5000)?;
// GET CONFIGURATION Feature 010Ch — MMC-6 §6.6
let mut gc = vec![0u8; 256];
let cdb_gc = [0x46, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00];
let result = transport.execute(&cdb_gc, DataDirection::FromDevice, &mut gc, 5000)?;
let firmware_date = if result.bytes_transferred > 12 {
String::from_utf8_lossy(&gc[12..24.min(result.bytes_transferred)])
.trim().to_string()
} else {
String::new()
};
Ok(Self::from_inquiry(&inquiry, &firmware_date))
}
/// Build identity from raw INQUIRY bytes and firmware date string.
pub fn from_inquiry(inquiry: &[u8], firmware_date: &str) -> Self {
DriveId {
vendor_id: ascii_field(inquiry, 8, 16),
product_id: ascii_field(inquiry, 16, 32),
product_revision: ascii_field(inquiry, 32, 36),
vendor_specific: ascii_field(inquiry, 36, 43),
firmware_date: firmware_date.to_string(),
raw_inquiry: inquiry.to_vec(),
}
}
/// Profile match key: "VENDOR|PRODUCT|REVISION|VENDOR_SPECIFIC"
///
/// Used to look up this drive in the profile database.
/// All fields trimmed for consistent matching.
pub fn match_key(&self) -> String {
format!("{}|{}|{}|{}",
self.vendor_id.trim(),
self.product_id.trim(),
self.product_revision.trim(),
self.vendor_specific.trim())
}
}
impl std::fmt::Display for DriveId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {} {} {}",
self.vendor_id.trim(),
self.product_id.trim(),
self.product_revision.trim(),
self.vendor_specific.trim())
}
}
/// Extract an ASCII string field from raw SCSI data.
fn ascii_field(data: &[u8], start: usize, end: usize) -> String {
if data.len() > start {
let e = end.min(data.len());
String::from_utf8_lossy(&data[start..e]).to_string()
} else {
String::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bu40n_identity() {
let mut inquiry = vec![0u8; 96];
inquiry[4] = 0x5B;
inquiry[8..16].copy_from_slice(b"HL-DT-ST");
inquiry[16..32].copy_from_slice(b"BD-RE BU40N ");
inquiry[32..36].copy_from_slice(b"1.03");
inquiry[36..43].copy_from_slice(b"NM00000");
let id = DriveId::from_inquiry(&inquiry, "211810241934");
assert_eq!(id.vendor_id.trim(), "HL-DT-ST");
assert_eq!(id.product_id.trim(), "BD-RE BU40N");
assert_eq!(id.product_revision.trim(), "1.03");
assert_eq!(id.vendor_specific.trim(), "NM00000");
assert_eq!(id.firmware_date, "211810241934");
assert_eq!(id.match_key(), "HL-DT-ST|BD-RE BU40N|1.03|NM00000");
}
#[test]
fn test_pioneer_identity() {
let mut inquiry = vec![0u8; 96];
inquiry[4] = 0x5B;
inquiry[8..16].copy_from_slice(b"PIONEER ");
inquiry[16..32].copy_from_slice(b"BD-RW BDR-S09 ");
inquiry[32..36].copy_from_slice(b"1.34");
inquiry[36..43].copy_from_slice(b" 16/04/");
let id = DriveId::from_inquiry(&inquiry, "201604250000");
assert_eq!(id.vendor_id.trim(), "PIONEER");
assert_eq!(id.product_id.trim(), "BD-RW BDR-S09");
assert_eq!(id.product_revision.trim(), "1.34");
assert_eq!(id.vendor_specific.trim(), "16/04/");
assert_eq!(id.firmware_date, "201604250000");
}
}
+52
View File
@@ -0,0 +1,52 @@
//! libfreemkv — Open source raw disc access for optical drives.
//!
//! Provides SCSI/MMC commands to enable raw reading mode on compatible
//! Blu-ray drives, allowing direct sector access for disc archival
//! and backup purposes.
//!
//! # Architecture
//!
//! The library is data-driven. Drive-specific SCSI command sequences
//! are stored in profile files, not in code. Adding support for a new
//! drive requires only a profile contribution — no rebuild needed.
//!
//! ```text
//! DriveSession (high-level API)
//! ├── Platform trait (per-chipset unlock logic)
//! ├── DriveProfile (per-drive data from JSON profiles)
//! └── ScsiTransport (SG_IO on Linux, IOKit on macOS)
//! ```
//!
//! # Quick Start
//!
//! ```no_run
//! use libfreemkv::DriveSession;
//! use std::path::Path;
//!
//! let mut session = DriveSession::open(
//! Path::new("/dev/sr0"),
//! Path::new("profiles/"),
//! ).unwrap();
//!
//! session.enable().unwrap();
//! session.calibrate().unwrap();
//!
//! let mut buf = vec![0u8; 2048];
//! let n = session.read_sectors(0, 1, &mut buf).unwrap();
//! ```
pub mod error;
pub mod scsi;
pub mod profile;
pub mod platform;
pub mod drive;
pub mod identity;
pub mod speed;
pub use error::{Error, Result};
pub use drive::DriveSession;
pub use identity::DriveId;
pub use profile::{DriveProfile, PlatformType};
pub use platform::{Platform, DriveStatus};
pub use scsi::ScsiTransport;
pub use speed::DriveSpeed;
+66
View File
@@ -0,0 +1,66 @@
//! Platform-specific implementations of raw disc access commands.
//!
//! Each chipset family (MT1959, Pioneer) implements the Platform trait.
//! accessed via SCSI READ BUFFER with platform-specific mode and buffer ID.
pub mod mt1959;
use crate::error::Result;
use crate::scsi::ScsiTransport;
/// Platform trait — raw disc access commands implemented per chipset.
///
/// Command handlers accessed via READ BUFFER:
pub trait Platform {
///
/// Sends the platform-specific READ BUFFER CDB and verifies
/// the response signature bytes.
fn unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
///
/// Performs a primary READ BUFFER for the configuration data,
/// followed by a secondary 4-byte status read.
fn read_config(&mut self, scsi: &mut dyn ScsiTransport) -> Result<Vec<u8>>;
/// Handlers 2-3: Read hardware register.
///
/// `index` selects which register offset from the profile to use.
/// Returns 16 bytes of register data extracted from a 36-byte response.
fn read_register(&mut self, scsi: &mut dyn ScsiTransport, index: u8) -> Result<[u8; 16]>;
///
/// Probes the disc surface via READ BUFFER sub-commands to build
/// a 64-entry speed lookup table for optimal read performance.
/// Issues SET CD SPEED at maximum after calibration completes.
fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
///
/// Periodic command to maintain the raw access session.
fn keepalive(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
///
/// Verifies the response signature and returns 16 bytes of
/// feature/status data.
fn status(&mut self, scsi: &mut dyn ScsiTransport) -> Result<DriveStatus>;
///
/// Sends a READ BUFFER command with dynamic sub-command, address,
/// and length. Used for disc structure reads and feature queries.
fn probe(&mut self, scsi: &mut dyn ScsiTransport, sub_cmd: u8, address: u32, length: u32) -> Result<Vec<u8>>;
///
/// Looks up the LBA in the speed table, issues SET CD SPEED,
/// then performs a READ(10) with the raw read flag (0x08).
fn read_sectors(&mut self, scsi: &mut dyn ScsiTransport, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize>;
fn timing(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>;
/// Check if raw disc access mode is currently enabled.
fn is_unlocked(&self) -> bool;
}
#[derive(Debug, Clone)]
pub struct DriveStatus {
pub unlocked: bool,
pub features: [u8; 16],
}
+329
View File
@@ -0,0 +1,329 @@
//! MT1959 platform implementation — covers all LG/ASUS MediaTek drives.
//!
//! Two variants share this code:
//! MT1959-A: mode=0x01, buffer_id=0x44 (handlers 0-9)
//! MT1959-B: mode=0x02, buffer_id=0x77 (handlers 4-9, 0-3 are no-ops)
//!
//! The logic is identical between A and B — only the SCSI READ BUFFER
//! mode and buffer ID differ. Per-drive data (signature, register offsets)
//! comes from the profile.
use crate::error::{Error, Result};
use crate::profile::DriveProfile;
use crate::scsi::{self, DataDirection, ScsiTransport};
use super::{Platform, DriveStatus};
/// MT1959 driver state.
pub struct Mt1959 {
profile: DriveProfile,
mode: u8,
buffer_id: u8,
unlocked: bool,
speed_table: [u16; 64],
calibrated: bool,
}
impl Mt1959 {
pub fn new(profile: DriveProfile) -> Self {
let mode = profile.platform.mode();
let buffer_id = profile.platform.buffer_id();
Mt1959 {
profile,
mode,
buffer_id,
unlocked: false,
speed_table: [0u16; 64],
calibrated: false,
}
}
/// Build a READ BUFFER CDB for this platform's mode and buffer ID.
fn read_buffer_cdb(&self, offset: u32, length: u32) -> [u8; 10] {
scsi::build_read_buffer(self.mode, self.buffer_id, offset, length)
}
/// Build a READ BUFFER CDB with a sub-command byte in CDB[3].
fn read_buffer_sub(&self, sub_cmd: u8, address: u16, length: u8) -> [u8; 10] {
[
0x3C,
self.mode,
self.buffer_id,
sub_cmd,
(address >> 8) as u8,
address as u8,
0x00,
0x00,
length,
0x00,
]
}
///
/// 1. Send READ BUFFER(mode, buffer_id, offset=0, length=64)
/// 2. Check response[0:4] matches the profile signature
/// 3. Check response[12:16] matches the verification bytes (0x4D4D6B76)
fn do_unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<[u8; 64]> {
let cdb = self.read_buffer_cdb(0, 64);
let mut response = [0u8; 64];
scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
// Check signature at response[0:4]
let got_sig: [u8; 4] = response[0..4].try_into().unwrap();
if got_sig != self.profile.signature {
return Err(Error::SignatureMismatch {
expected: self.profile.signature,
got: got_sig,
});
}
// Check verification bytes at response[12:16]
if &response[12..16] != self.profile.verify.as_slice() {
return Err(Error::UnlockFailed(format!(
"verify mismatch at [12:16]: {:02x}{:02x}{:02x}{:02x}",
response[12], response[13], response[14], response[15]
)));
}
self.unlocked = true;
Ok(response)
}
/// Ensure raw disc access is active, re-enabling if needed.
fn ensure_unlocked(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
if !self.unlocked {
self.do_unlock(scsi)?;
}
Ok(())
}
/// Pre-operation validation with retry.
///
/// Sends a short READ BUFFER probe, retries up to 5 times to confirm
/// the drive is still responding to commands.
fn validate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
for _attempt in 0..5 {
let cdb = self.read_buffer_cdb(0, 4);
let mut resp = [0u8; 4];
match scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000) {
Ok(_) => return Ok(()),
Err(_) => continue,
}
}
Err(Error::ScsiError {
cdb: vec![0x3C],
status: 0xFF,
sense: vec![],
})
}
/// Look up optimal read speed for a given LBA from the calibration table.
fn lookup_speed(&self, lba: u32) -> u16 {
if !self.calibrated {
return 0;
}
let mut best_speed = 0u16;
let mut best_diff = u32::MAX;
for &entry in &self.speed_table {
if entry == 0 {
continue;
}
let entry_lba = entry as u32;
let diff = if lba > entry_lba { lba - entry_lba } else { entry_lba - lba };
if diff < best_diff {
best_diff = diff;
best_speed = entry;
}
}
best_speed
}
/// Send SET CD SPEED command.
fn set_cd_speed(&self, scsi: &mut dyn ScsiTransport, speed: u16) -> Result<()> {
let cdb = scsi::build_set_cd_speed(speed);
let mut dummy = [0u8; 0];
scsi.execute(&cdb, DataDirection::None, &mut dummy, 5_000)?;
Ok(())
}
}
impl Platform for Mt1959 {
fn unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
self.do_unlock(scsi)?;
Ok(())
}
///
/// Primary read: 0x760 (1888) bytes of configuration data.
/// Secondary read: 4-byte status appended to the result.
fn read_config(&mut self, scsi: &mut dyn ScsiTransport) -> Result<Vec<u8>> {
// Primary config read: 0x760 = 1888 bytes
let cdb = self.read_buffer_cdb(0, 0x760);
let mut buf = vec![0u8; 0x760];
let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 30_000)?;
buf.truncate(result.bytes_transferred);
// Secondary: 4-byte status read
let cdb2 = self.read_buffer_cdb(0, 4);
let mut status = [0u8; 4];
scsi.execute(&cdb2, DataDirection::FromDevice, &mut status, 5_000)?;
buf.extend_from_slice(&status);
Ok(buf)
}
/// Handlers 2-3: Read hardware register at the profile-specified offset.
///
/// Reads 36 bytes via READ BUFFER and extracts bytes [4:20] as the
/// 16-byte register value.
fn read_register(&mut self, scsi: &mut dyn ScsiTransport, index: u8) -> Result<[u8; 16]> {
self.ensure_unlocked(scsi)?;
self.validate(scsi)?;
let offset = *self.profile.register_offsets.get(index as usize)
.ok_or_else(|| Error::ScsiError {
cdb: vec![],
status: 0,
sense: vec![],
})?;
let cdb = scsi::build_read_buffer(self.mode, self.buffer_id, offset, 36);
let mut response = [0u8; 36];
scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
let mut out = [0u8; 16];
out.copy_from_slice(&response[4..20]);
Ok(out)
}
///
/// Scans disc surface addresses via READ BUFFER sub-command 0x14 to
/// build a 64-entry speed lookup table. Issues SET CD SPEED(max) when done.
fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> {
self.ensure_unlocked(scsi)?;
self.validate(scsi)?;
// Initial probe: READ BUFFER sub_cmd=0x12
let cdb = self.read_buffer_sub(0x12, 0, 4);
let mut resp = [0u8; 4];
let _ = scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000);
self.validate(scsi)?;
// Clear speed table
self.speed_table = [0u16; 64];
// Scan disc surface — probe addresses up to 0x10000, 256 at a time
let mut table_idx = 0usize;
let mut addr: u32 = 0;
while addr < 0x10000 && table_idx < 64 {
let cdb = self.read_buffer_sub(0x14, addr as u16, 4);
let mut resp = [0u8; 4];
match scsi.execute(&cdb, DataDirection::FromDevice, &mut resp, 5_000) {
Ok(r) if r.bytes_transferred == 4 => {
let val = resp[0];
if val > 0 {
let speed_entry = ((resp[0] as u16) << 8) | (resp[1] as u16);
if speed_entry > 0 {
self.speed_table[table_idx] = speed_entry;
table_idx += 1;
}
}
addr += 256;
}
_ => {
addr += 256;
}
}
}
// Set max speed after calibration
self.set_cd_speed(scsi, 0xFFFF)?;
self.calibrated = true;
Ok(())
}
fn keepalive(&mut self, _scsi: &mut dyn ScsiTransport) -> Result<()> {
Ok(())
}
///
/// Sends READ BUFFER with sub-command 0x13, reads 36 bytes.
/// Checks signature at [0:4], returns feature data from [4:20].
fn status(&mut self, scsi: &mut dyn ScsiTransport) -> Result<DriveStatus> {
self.ensure_unlocked(scsi)?;
self.validate(scsi)?;
// READ BUFFER with sub_cmd=0x13, 36 bytes response
let cdb = self.read_buffer_sub(0x13, 0, 36);
let mut response = [0u8; 36];
scsi.execute(&cdb, DataDirection::FromDevice, &mut response, 30_000)?;
// Verify response signature
let got_sig = u32::from_be_bytes(response[0..4].try_into().unwrap());
let expected_sig = u32::from_le_bytes(self.profile.signature);
let mut features = [0u8; 16];
features.copy_from_slice(&response[4..20]);
Ok(DriveStatus {
unlocked: got_sig == expected_sig,
features,
})
}
fn probe(&mut self, scsi: &mut dyn ScsiTransport, sub_cmd: u8, address: u32, length: u32) -> Result<Vec<u8>> {
let cdb = [
0x3C,
self.mode,
self.buffer_id,
sub_cmd,
(address >> 16) as u8,
(address >> 8) as u8,
address as u8,
(length >> 16) as u8,
(length >> 8) as u8,
length as u8,
];
let mut buf = vec![0u8; length as usize];
let result = scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 30_000)?;
buf.truncate(result.bytes_transferred);
Ok(buf)
}
///
/// Looks up the LBA in the speed table, issues SET CD SPEED if calibrated,
/// then performs READ(10) with the raw read flag (0x08).
fn read_sectors(
&mut self,
scsi: &mut dyn ScsiTransport,
lba: u32,
count: u16,
buf: &mut [u8],
) -> Result<usize> {
if !self.unlocked {
return Err(Error::NotUnlocked);
}
// Speed optimization from calibration
if self.calibrated {
let speed = self.lookup_speed(lba);
if speed > 0 {
let _ = self.set_cd_speed(scsi, speed);
}
}
// READ(10) with raw flag 0x08
let cdb = scsi::build_read10_raw(lba, count);
let result = scsi.execute(&cdb, DataDirection::FromDevice, buf, 30_000)?;
Ok(result.bytes_transferred)
}
fn timing(&mut self, _scsi: &mut dyn ScsiTransport) -> Result<()> {
Ok(())
}
fn is_unlocked(&self) -> bool {
self.unlocked
}
}
+337
View File
@@ -0,0 +1,337 @@
//! Drive profile loading and matching.
//!
//! Each supported drive has a profile containing the SCSI command
//! parameters needed to enable raw disc access mode. Profiles are
//! loaded from JSON files so new drives can be added without rebuilding.
use serde::Deserialize;
use crate::error::{Error, Result};
/// Per-drive profile containing SCSI parameters for raw disc access.
#[derive(Debug, Clone, Deserialize)]
pub struct DriveProfile {
/// Drive vendor from INQUIRY[8:16] (e.g. "HL-DT-ST")
#[serde(default)]
pub vendor_id: String,
/// Drive product (devtype) from INQUIRY product field (e.g. "BD-RE")
#[serde(default)]
pub product_id: String,
/// Firmware revision from INQUIRY[32:36] (e.g. "1.03")
#[serde(default)]
pub product_revision: String,
/// Firmware type from INQUIRY[36:43] (e.g. "NM00000")
#[serde(default)]
pub vendor_specific: String,
/// Firmware build date from GET_CONFIG 010C (e.g. "211810241934")
#[serde(default)]
pub firmware_date: String,
/// Chipset platform type determining the READ BUFFER variant.
#[serde(default)]
pub platform: PlatformType,
/// Whether this drive supports raw disc access mode.
#[serde(default)]
pub supported: bool,
/// Current readiness status of this drive.
#[serde(default)]
pub status: ReadinessStatus,
/// Drive identifier string from the profile database.
#[serde(default)]
pub drive_id: String,
/// Profile version string.
#[serde(default)]
pub drive_version: String,
/// Expected response signature bytes [0:4] from the enable command.
#[serde(default, deserialize_with = "deserialize_hex4")]
pub signature: [u8; 4],
/// Expected verification bytes [12:16] from the enable response.
#[serde(skip, default = "default_verify")]
pub verify: [u8; 4],
/// 10-byte READ BUFFER CDB used to enable raw disc access.
#[serde(default, deserialize_with = "deserialize_hex_vec")]
pub unlock_cdb: Vec<u8>,
/// Register read offsets (bytes 3-5 of READ BUFFER CDB).
#[serde(default)]
pub register_offsets: Vec<u32>,
/// Drive supports reading DVDs regardless of region code.
#[serde(default)]
pub dvd_all_regions: bool,
/// Drive supports raw Blu-ray sector reads.
#[serde(default)]
pub bd_raw_read: bool,
/// Drive supports raw Blu-ray metadata reads.
#[serde(default)]
pub bd_raw_metadata: bool,
/// Drive supports unrestricted read speed.
#[serde(default)]
pub unrestricted_speed: bool,
}
fn default_verify() -> [u8; 4] {
*b"MMkv"
}
/// Chipset platform type. Determines the READ BUFFER mode and buffer ID.
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
pub enum PlatformType {
/// MediaTek MT1959 variant A: mode=0x01, buffer_id=0x44.
#[serde(rename = "mt1959_a")]
Mt1959A,
/// MediaTek MT1959 variant B: mode=0x02, buffer_id=0x77.
#[serde(rename = "mt1959_b")]
Mt1959B,
/// Pioneer chipset (not yet implemented).
#[serde(rename = "pioneer")]
Pioneer,
}
impl Default for PlatformType {
fn default() -> Self {
PlatformType::Mt1959A
}
}
/// Readiness status of a drive for raw disc access.
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
pub enum ReadinessStatus {
/// Drive is ready — raw disc access can be enabled.
Ready,
/// Drive firmware needs an update before raw access is possible.
NeedsFirmwareUpdate,
/// Drive uses encrypted commands (not yet supported).
Encrypted,
/// Status unknown.
Unknown,
}
impl Default for ReadinessStatus {
fn default() -> Self {
ReadinessStatus::Unknown
}
}
impl PlatformType {
/// Human-readable name for this platform.
pub fn name(&self) -> &'static str {
match self {
PlatformType::Mt1959A => "MT1959-A",
PlatformType::Mt1959B => "MT1959-B",
PlatformType::Pioneer => "Pioneer",
}
}
/// READ BUFFER mode byte for this chipset platform.
pub fn mode(&self) -> u8 {
match self {
PlatformType::Mt1959A => 0x01,
PlatformType::Mt1959B => 0x02,
PlatformType::Pioneer => 0x01, // TBD
}
}
/// READ BUFFER buffer ID for this chipset platform.
pub fn buffer_id(&self) -> u8 {
match self {
PlatformType::Mt1959A => 0x44,
PlatformType::Mt1959B => 0x77,
PlatformType::Pioneer => 0x44, // TBD
}
}
}
/// Parse a hex string like "999ec375" into [u8; 4].
fn parse_hex4(s: &str) -> Result<[u8; 4]> {
if s.len() != 8 {
return Err(Error::ProfileParse(format!("expected 8 hex chars, got {}", s.len())));
}
let mut out = [0u8; 4];
for i in 0..4 {
out[i] = u8::from_str_radix(&s[i*2..i*2+2], 16)
.map_err(|e| Error::ProfileParse(format!("bad hex: {e}")))?;
}
Ok(out)
}
/// Parse a hex string into a byte vector.
fn parse_hex(s: &str) -> Result<Vec<u8>> {
if s.len() % 2 != 0 {
return Err(Error::ProfileParse("odd hex length".into()));
}
let mut out = Vec::with_capacity(s.len() / 2);
for i in (0..s.len()).step_by(2) {
out.push(u8::from_str_radix(&s[i..i+2], 16)
.map_err(|e| Error::ProfileParse(format!("bad hex: {e}")))?);
}
Ok(out)
}
/// Custom serde deserializer for 4-byte hex signature strings.
fn deserialize_hex4<'de, D>(deserializer: D) -> std::result::Result<[u8; 4], D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_hex4(&s).map_err(serde::de::Error::custom)
}
/// Custom serde deserializer for hex-encoded byte vectors.
fn deserialize_hex_vec<'de, D>(deserializer: D) -> std::result::Result<Vec<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_hex(&s).map_err(serde::de::Error::custom)
}
/// Load a profile from a parsed JSON value.
pub fn load_from_json(json: &serde_json::Value) -> Result<DriveProfile> {
let vendor = json["vendor_id"].as_str().unwrap_or("").to_string();
let product = json["product_id"].as_str().unwrap_or("").to_string();
let revision = json["product_revision"].as_str().unwrap_or("").to_string();
let firmware_type = json["vendor_specific"].as_str().unwrap_or("").to_string();
let firmware_date = json["firmware_date"].as_str().unwrap_or("").to_string();
let program = json["program"].as_str().unwrap_or("unknown");
let platform = match program {
"mt1959_a" => PlatformType::Mt1959A,
"mt1959_b" => PlatformType::Mt1959B,
_ => PlatformType::Mt1959A, // default
};
let sig_str = json["signature"].as_str().unwrap_or("");
// Drive is supported if it has a known program and valid signature
let has_program = matches!(program, "mt1959_a" | "mt1959_b");
let has_signature = sig_str.len() == 8;
let supported = has_program && has_signature;
let status = if supported {
ReadinessStatus::Ready
} else if json["status"].as_str() == Some("needs_flash") || program == "none" {
ReadinessStatus::NeedsFirmwareUpdate
} else {
ReadinessStatus::Unknown
};
let signature = if sig_str.len() == 8 {
parse_hex4(sig_str)?
} else {
[0; 4]
};
let unlock_cdb = json["unlock_cdb"].as_str()
.map(|s| parse_hex(s))
.transpose()?
.unwrap_or_default();
let register_offsets = json["register_cdbs"].as_array()
.map(|arr| {
arr.iter().filter_map(|v| {
let s = v.as_str()?;
// CDB format: 3c 01 44 XX XX XX 00 00 24 00
// Register offset is bytes 3-5 (chars 6-12 in hex)
if s.len() >= 12 {
u32::from_str_radix(&s[6..12], 16).ok()
} else {
None
}
}).collect()
})
.unwrap_or_default();
Ok(DriveProfile {
vendor_id: vendor,
product_id: product,
product_revision: revision,
vendor_specific: firmware_type,
firmware_date,
platform,
supported,
status,
drive_id: json["drive_id"].as_str().unwrap_or("").to_string(),
drive_version: json["drive_version"].as_str().unwrap_or("").to_string(),
signature,
verify: *b"MMkv",
unlock_cdb,
register_offsets,
dvd_all_regions: json["capabilities"]["dvd_all_regions"].as_bool().unwrap_or(false),
bd_raw_read: json["capabilities"]["bd_raw_read"].as_bool().unwrap_or(false),
bd_raw_metadata: json["capabilities"]["bd_raw_metadata"].as_bool().unwrap_or(false),
unrestricted_speed: json["capabilities"]["unrestricted_speed"].as_bool().unwrap_or(false),
})
}
/// Bundled profiles — compiled into the binary.
/// Override with load_all() to load from a file instead.
const BUNDLED_PROFILES: &str = include_str!("../profiles.json");
/// Load profiles from the bundled database.
pub fn load_bundled() -> Result<Vec<DriveProfile>> {
load_from_str(BUNDLED_PROFILES)
}
/// Load all profiles from a JSON array file.
pub fn load_all(path: &std::path::Path) -> Result<Vec<DriveProfile>> {
let data = std::fs::read_to_string(path)?;
load_from_str(&data)
}
/// Parse profiles from a JSON string.
fn load_from_str(data: &str) -> Result<Vec<DriveProfile>> {
let json: serde_json::Value = serde_json::from_str(data)
.map_err(|e| Error::ProfileParse(format!("JSON: {e}")))?;
let arr = json.as_array()
.ok_or_else(|| Error::ProfileParse("expected array".into()))?;
let mut profiles = Vec::with_capacity(arr.len());
for entry in arr {
match load_from_json(entry) {
Ok(p) => profiles.push(p),
Err(_) => continue, // skip malformed entries
}
}
Ok(profiles)
}
/// Find a profile matching a drive's INQUIRY fields.
///
/// Matches by vendor + product + revision + vendor_specific (firmware type).
/// All fields trimmed before comparison.
pub fn find_by_drive_id<'a>(
profiles: &'a [DriveProfile],
drive_id: &crate::identity::DriveId,
) -> Option<&'a DriveProfile> {
let v = drive_id.vendor_id.trim();
let r = drive_id.product_revision.trim();
let vs = drive_id.vendor_specific.trim();
// Match all four INQUIRY fields for precise identification
profiles.iter().find(|p| {
p.vendor_id.trim() == v
&& p.product_revision.trim() == r
&& p.vendor_specific.trim() == vs
&& p.firmware_date.trim() == drive_id.firmware_date.trim()
})
// Fallback: match without date (for drives where 010C isn't available)
.or_else(|| profiles.iter().find(|p| {
p.vendor_id.trim() == v
&& p.product_revision.trim() == r
&& p.vendor_specific.trim() == vs
}))
}
+215
View File
@@ -0,0 +1,215 @@
//! SCSI/MMC command interface via Linux SG_IO.
use crate::error::{Error, Result};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DataDirection {
None,
FromDevice,
ToDevice,
}
#[derive(Debug)]
pub struct ScsiResult {
pub status: u8,
pub bytes_transferred: usize,
pub sense: [u8; 32],
}
/// Low-level SCSI transport.
pub trait ScsiTransport {
fn execute(
&mut self,
cdb: &[u8],
direction: DataDirection,
data: &mut [u8],
timeout_ms: u32,
) -> Result<ScsiResult>;
}
/// Linux SG_IO transport.
pub struct SgIoTransport {
fd: i32,
}
// SG_IO constants
const SG_IO: libc::c_ulong = 0x2285;
const SG_DXFER_NONE: i32 = -1;
const SG_DXFER_TO_DEV: i32 = -2;
const SG_DXFER_FROM_DEV: i32 = -3;
#[repr(C)]
#[allow(non_camel_case_types)]
struct sg_io_hdr {
interface_id: i32,
dxfer_direction: i32,
cmd_len: u8,
mx_sb_len: u8,
iovec_count: u16,
dxfer_len: u32,
dxferp: *mut u8,
cmdp: *const u8,
sbp: *mut u8,
timeout: u32,
flags: u32,
pack_id: i32,
usr_ptr: *mut libc::c_void,
status: u8,
masked_status: u8,
msg_status: u8,
sb_len_wr: u8,
host_status: u16,
driver_status: u16,
resid: i32,
duration: u32,
info: u32,
}
impl SgIoTransport {
pub fn open(device: &Path) -> Result<Self> {
use std::os::unix::ffi::OsStrExt;
let path_bytes = device.as_os_str().as_bytes();
let mut c_path = Vec::with_capacity(path_bytes.len() + 1);
c_path.extend_from_slice(path_bytes);
c_path.push(0);
let fd = unsafe { libc::open(c_path.as_ptr() as *const libc::c_char, libc::O_RDWR | libc::O_NONBLOCK) };
if fd < 0 {
return Err(Error::DeviceNotFound(device.display().to_string()));
}
Ok(SgIoTransport { fd })
}
}
impl Drop for SgIoTransport {
fn drop(&mut self) {
unsafe { libc::close(self.fd); }
}
}
impl ScsiTransport for SgIoTransport {
fn execute(
&mut self,
cdb: &[u8],
direction: DataDirection,
data: &mut [u8],
timeout_ms: u32,
) -> Result<ScsiResult> {
let mut sense = [0u8; 32];
let dxfer_direction = match direction {
DataDirection::None => SG_DXFER_NONE,
DataDirection::FromDevice => SG_DXFER_FROM_DEV,
DataDirection::ToDevice => SG_DXFER_TO_DEV,
};
let mut hdr: sg_io_hdr = unsafe { std::mem::zeroed() };
hdr.interface_id = b'S' as i32;
hdr.dxfer_direction = dxfer_direction;
hdr.cmd_len = cdb.len() as u8;
hdr.mx_sb_len = sense.len() as u8;
hdr.dxfer_len = data.len() as u32;
hdr.dxferp = data.as_mut_ptr();
hdr.cmdp = cdb.as_ptr();
hdr.sbp = sense.as_mut_ptr();
hdr.timeout = timeout_ms;
let ret = unsafe {
libc::ioctl(self.fd, SG_IO, &mut hdr as *mut sg_io_hdr)
};
if ret < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
let bytes_transferred = (data.len() as i32 - hdr.resid) as usize;
if hdr.status != 0 {
return Err(Error::ScsiError {
cdb: cdb.to_vec(),
status: hdr.status,
sense: sense[..hdr.sb_len_wr as usize].to_vec(),
});
}
Ok(ScsiResult {
status: hdr.status,
bytes_transferred,
sense,
})
}
}
/// SCSI INQUIRY response.
#[derive(Debug, Clone)]
pub struct InquiryResult {
pub vendor_id: String,
pub model: String,
pub firmware: String,
pub raw: Vec<u8>,
}
/// Send INQUIRY command and parse the standard response fields.
pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result<InquiryResult> {
let cdb = [0x12, 0x00, 0x00, 0x00, 0x60, 0x00];
let mut buf = [0u8; 96];
scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?;
let vendor = String::from_utf8_lossy(&buf[8..16]).trim().to_string();
let model = String::from_utf8_lossy(&buf[16..32]).trim().to_string();
let firmware = String::from_utf8_lossy(&buf[32..36]).trim().to_string();
Ok(InquiryResult {
vendor_id: vendor,
model,
firmware,
raw: buf.to_vec(),
})
}
/// Send GET CONFIGURATION for feature 0x010C (drive serial number).
pub fn get_config_010c(scsi: &mut dyn ScsiTransport) -> Result<Vec<u8>> {
let cdb = [0x46, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00];
let mut buf = [0u8; 16];
scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?;
Ok(buf.to_vec())
}
/// Build a READ BUFFER (0x3C) CDB with the given mode, buffer ID, offset, and length.
pub fn build_read_buffer(mode: u8, buffer_id: u8, offset: u32, length: u32) -> [u8; 10] {
[
0x3C, // READ BUFFER
mode,
buffer_id,
(offset >> 16) as u8,
(offset >> 8) as u8,
offset as u8,
(length >> 16) as u8,
(length >> 8) as u8,
length as u8,
0x00,
]
}
/// Build a SET CD SPEED (0xBB) CDB with the given read speed.
pub fn build_set_cd_speed(read_speed: u16) -> [u8; 12] {
[
0xBB, 0x00,
(read_speed >> 8) as u8, read_speed as u8,
0xFF, 0xFF, // write speed = max
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
}
/// Build a READ(10) CDB with the raw read flag (0x08) set.
pub fn build_read10_raw(lba: u32, count: u16) -> [u8; 10] {
[
0x28, 0x08, // READ(10), flag=0x08 (raw)
(lba >> 24) as u8, (lba >> 16) as u8,
(lba >> 8) as u8, lba as u8,
0x00,
(count >> 8) as u8, count as u8,
0x00,
]
}
+125
View File
@@ -0,0 +1,125 @@
//! Drive speed control — query and set read speeds.
//!
//! Uses MMC-6 SET CD SPEED (0xBB) command.
//! Reference: MMC-6 §6.30
/// Disc read speed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DriveSpeed {
/// Blu-ray 1x = 4,500 KB/s
BD1x,
/// Blu-ray 2x = 9,000 KB/s
BD2x,
/// Blu-ray 4x = 18,000 KB/s
BD4x,
/// Blu-ray 6x = 27,000 KB/s
BD6x,
/// Blu-ray 8x = 36,000 KB/s
BD8x,
/// Blu-ray 10x = 45,000 KB/s
BD10x,
/// Blu-ray 12x = 54,000 KB/s
BD12x,
/// DVD 1x = 1,385 KB/s
DVD1x,
/// DVD 2x = 2,770 KB/s
DVD2x,
/// DVD 4x = 5,540 KB/s
DVD4x,
/// DVD 8x = 11,080 KB/s
DVD8x,
/// DVD 16x = 22,160 KB/s
DVD16x,
/// Maximum speed — drive decides
Max,
}
impl DriveSpeed {
/// Convert to KB/s for MMC-6 SET CD SPEED command.
pub fn to_kbps(self) -> u16 {
match self {
DriveSpeed::BD1x => 4_500,
DriveSpeed::BD2x => 9_000,
DriveSpeed::BD4x => 18_000,
DriveSpeed::BD6x => 27_000,
DriveSpeed::BD8x => 36_000,
DriveSpeed::BD10x => 45_000,
DriveSpeed::BD12x => 54_000,
DriveSpeed::DVD1x => 1_385,
DriveSpeed::DVD2x => 2_770,
DriveSpeed::DVD4x => 5_540,
DriveSpeed::DVD8x => 11_080,
DriveSpeed::DVD16x => 22_160,
DriveSpeed::Max => 0xFFFF,
}
}
/// Create from KB/s value, rounding to nearest standard speed.
pub fn from_kbps(kbps: u16) -> Self {
match kbps {
0..=2_000 => DriveSpeed::DVD1x,
2_001..=4_000 => DriveSpeed::DVD2x,
4_001..=6_000 => DriveSpeed::BD1x,
6_001..=13_000 => DriveSpeed::BD2x,
13_001..=22_000 => DriveSpeed::BD4x,
22_001..=31_000 => DriveSpeed::BD6x,
31_001..=40_000 => DriveSpeed::BD8x,
40_001..=49_000 => DriveSpeed::BD10x,
49_001..=0xFFFE => DriveSpeed::BD12x,
0xFFFF => DriveSpeed::Max,
_ => DriveSpeed::Max,
}
}
/// Human-readable label.
pub fn label(&self) -> &'static str {
match self {
DriveSpeed::BD1x => "BD 1x",
DriveSpeed::BD2x => "BD 2x",
DriveSpeed::BD4x => "BD 4x",
DriveSpeed::BD6x => "BD 6x",
DriveSpeed::BD8x => "BD 8x",
DriveSpeed::BD10x => "BD 10x",
DriveSpeed::BD12x => "BD 12x",
DriveSpeed::DVD1x => "DVD 1x",
DriveSpeed::DVD2x => "DVD 2x",
DriveSpeed::DVD4x => "DVD 4x",
DriveSpeed::DVD8x => "DVD 8x",
DriveSpeed::DVD16x => "DVD 16x",
DriveSpeed::Max => "Max",
}
}
/// All standard Blu-ray speeds.
pub fn all_bd() -> &'static [DriveSpeed] {
&[DriveSpeed::BD1x, DriveSpeed::BD2x, DriveSpeed::BD4x,
DriveSpeed::BD6x, DriveSpeed::BD8x, DriveSpeed::BD10x, DriveSpeed::BD12x]
}
/// All standard DVD speeds.
pub fn all_dvd() -> &'static [DriveSpeed] {
&[DriveSpeed::DVD1x, DriveSpeed::DVD2x, DriveSpeed::DVD4x,
DriveSpeed::DVD8x, DriveSpeed::DVD16x]
}
}
impl std::fmt::Display for DriveSpeed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({} KB/s)", self.label(), self.to_kbps())
}
}
/// Build SET CD SPEED CDB — MMC-6 §6.30
pub fn set_cd_speed_cdb(read_speed: DriveSpeed) -> [u8; 12] {
let kbps = read_speed.to_kbps();
[
0xBB, // SET CD SPEED opcode
0x00, // reserved
(kbps >> 8) as u8, // read speed MSB
kbps as u8, // read speed LSB
0xFF, // write speed MSB (0xFFFF = don't change)
0xFF, // write speed LSB
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x00, // reserved
]
}