v0.6.0: Clean API, chipset-keyed profiles, streamlined platform driver

- API: open() is OEM-only, wait_ready() separate, init() optional
- Profiles: chipset-keyed JSON ({ "mt1959": [...], "renesas": [] })
- Profiles: identity group, variant + signature + firmware per drive
- Platform constants: mode, buffer_id, nominal speed, verify commands
  moved from profiles to code (variant-determined, not per-drive)
- Removed unused fields: register CDBs, speed tables, status data
- Platform driver: unlock + firmware upload + calibrate + speed only
- Cross-compile fix: build.rs uses CARGO_CFG_TARGET_OS for framework linking
This commit is contained in:
MattJackson
2026-04-09 12:38:02 -07:00
parent b454d100c6
commit 0c11623666
7 changed files with 2415 additions and 5930 deletions
+26 -78
View File
@@ -1,85 +1,59 @@
//! Drive session — open, identify, unlock, and read from optical drives.
//! Drive session — open, identify, and read from optical drives.
//!
//! `DriveSession` is the entry point for all drive interaction. It handles
//! device identification, profile matching, platform-specific unlock, and
//! provides both raw sector reads and standard SCSI command execution.
//! device identification, profile matching, and provides both raw sector
//! reads and standard SCSI command execution.
//!
//! Two open modes:
//! - `open()` — identify + unlock. Ready for reading immediately.
//! - `open_no_unlock()` — identify only. Used for AACS authentication
//! which must happen before the drive enters raw mode.
//! Three-step open:
//! 1. `open()` — open device, identify drive. Always OEM.
//! 2. `wait_ready()` — wait for disc to spin up. Call before reading.
//! 3. `init()` — activate custom firmware. Optional, caller decides.
use std::path::Path;
use crate::error::{Error, Result};
use crate::scsi::ScsiTransport;
use crate::identity::DriveId;
use crate::profile::{self, DriveProfile, Chipset};
use crate::profile::{self, DriveProfile, Chipset, ProfileMatch};
use crate::platform::Platform;
use crate::platform::mt1959::Mt1959;
/// A drive session with identification, platform, and SCSI transport.
///
/// Created via `DriveSession::open()` or `DriveSession::open_no_unlock()`.
/// Created via `DriveSession::open()`.
/// All disc reading goes through this struct.
pub struct DriveSession {
scsi: Box<dyn ScsiTransport>,
platform: Box<dyn Platform>,
pub profile: DriveProfile,
pub chipset: Chipset,
pub drive_id: DriveId,
device_path: String,
}
impl DriveSession {
/// Open a drive — SCSI transport + INQUIRY identify.
///
/// This is the only entry point. After `open()`, the drive is
/// ready for scanning and content reads. init() handles everything:
/// unlock, firmware upload if needed, calibration, registers.
/// Called once per session. Non-fatal if init fails (BD works without it).
/// Pure OEM. No disc needed, no custom firmware.
/// Call `wait_ready()` before reading, `init()` for custom firmware.
pub fn open(device: &Path) -> Result<Self> {
let mut session = Self::open_no_unlock(device)?;
session.wait_ready()?;
let _ = session.init();
Ok(session)
}
/// Open a drive — identify only, no wait, no unlock.
///
/// Low-level entry point. Caller is responsible for wait_ready()
/// and unlock() ordering.
pub fn open_no_unlock(device: &Path) -> Result<Self> {
let mut transport = crate::scsi::open(device)?;
let profiles = profile::load_bundled()?;
let drive_id = DriveId::from_drive(transport.as_mut())?;
let profile = profile::find_by_drive_id(&profiles, &drive_id)
.cloned()
let m = profile::find_by_drive_id(&profiles, &drive_id)
.ok_or_else(|| Error::UnsupportedDrive {
vendor_id: drive_id.vendor_id.trim().to_string(),
product_id: drive_id.product_id.trim().to_string(),
product_revision: drive_id.product_revision.trim().to_string(),
})?;
let platform = create_platform(&profile, &drive_id)?;
let platform = create_platform(m.chipset, &m.profile)?;
Ok(DriveSession {
scsi: transport,
platform,
profile,
drive_id,
device_path: device.to_string_lossy().to_string(),
})
}
/// Open with an explicit profile, skipping auto-detection.
pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result<Self> {
let mut transport = crate::scsi::open(device)?;
let drive_id = DriveId::from_drive(transport.as_mut())?;
let platform = create_platform(&profile, &drive_id)?;
Ok(DriveSession {
scsi: transport,
platform,
profile,
chipset: m.chipset,
profile: m.profile,
drive_id,
device_path: device.to_string_lossy().to_string(),
})
@@ -88,7 +62,7 @@ impl DriveSession {
/// Wait for the drive to become ready (disc spun up).
/// Polls TEST UNIT READY up to 30 seconds.
pub fn wait_ready(&mut self) -> Result<()> {
let tur = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; // TEST UNIT READY
let tur = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
for _ in 0..60 {
let mut buf = [0u8; 0];
if self.scsi.as_mut().execute(
@@ -108,10 +82,10 @@ impl DriveSession {
&self.device_path
}
/// Activate custom firmware — unlock, upload firmware if needed, calibrate.
///
/// This is the ONLY entry point for activating raw disc access.
/// Handles the full x86 dispatch sequence internally:
/// unlock → [load_firmware if cold] × 6 → calibrate × 6 → registers
/// Optional. BD/DVD work without this (OEM, standard speed).
/// Required for UHD (AACS 2.0 bus encryption).
pub fn init(&mut self) -> Result<()> {
self.platform.init(self.scsi.as_mut())
}
@@ -121,7 +95,7 @@ impl DriveSession {
self.platform.is_ready()
}
/// Called per zone change during content reads.
/// Set read speed for a disc zone.
pub fn set_read_speed(&mut self, lba: u32) -> Result<()> {
self.platform.set_read_speed(self.scsi.as_mut(), lba)
}
@@ -155,16 +129,11 @@ impl DriveSession {
}
/// Eject the disc tray.
///
/// Sends PREVENT ALLOW MEDIUM REMOVAL (allow) first to release any
/// locks, then START STOP UNIT with LoEj=1 to open the tray.
pub fn eject(&mut self) -> Result<()> {
// PREVENT ALLOW MEDIUM REMOVAL: allow removal
let allow_cdb = [0x1Eu8, 0, 0, 0, 0x00, 0];
let mut buf = [0u8; 0];
let _ = self.scsi.as_mut().execute(&allow_cdb, crate::scsi::DataDirection::None, &mut buf, 5_000);
// START STOP UNIT: LoEj=1, Start=0
let eject_cdb = [0x1Bu8, 0, 0, 0, 0x02, 0];
self.scsi.as_mut().execute(&eject_cdb, crate::scsi::DataDirection::None, &mut buf, 30_000)?;
Ok(())
@@ -183,13 +152,6 @@ impl DriveSession {
}
/// Discover optical drives on the system.
///
/// Scans `/dev/sg0` through `/dev/sg15` (Linux SCSI Generic devices),
/// sends INQUIRY to each, and returns paths for optical drives (device type 5).
/// Always uses sg devices — sr devices have kernel-level speed management
/// that interferes with raw disc access.
///
/// Returns a list of (device_path, DriveId) for each found drive.
pub fn find_drives() -> Vec<(String, DriveId)> {
let mut drives = Vec::new();
for i in 0..16 {
@@ -199,8 +161,6 @@ pub fn find_drives() -> Vec<(String, DriveId)> {
}
if let Ok(mut transport) = crate::scsi::open(std::path::Path::new(&path)) {
if let Ok(id) = DriveId::from_drive(transport.as_mut()) {
// INQUIRY device type 5 = CD/DVD/BD
// We check by trying to match a profile — only optical drives have profiles
let profiles = match profile::load_bundled() {
Ok(p) => p,
Err(_) => continue,
@@ -215,18 +175,12 @@ pub fn find_drives() -> Vec<(String, DriveId)> {
}
/// Find the first optical drive on the system.
/// Returns the sg device path, or None if no drive found.
pub fn find_drive() -> Option<String> {
find_drives().into_iter().next().map(|(path, _)| path)
}
/// Resolve a device path to the correct sg device.
///
/// If the user passes `/dev/sr0`, maps it to the corresponding `/dev/sg*`.
/// If they pass `/dev/sg*`, validates it exists.
/// Returns `(resolved_path, warning)` where warning is set if the path was remapped.
pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
// Already an sg device — use as-is
if path.contains("/sg") {
if !std::path::Path::new(path).exists() {
return Err(Error::DeviceNotFound { path: path.to_string() });
@@ -234,14 +188,11 @@ pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
return Ok((path.to_string(), None));
}
// sr device — find the matching sg device by comparing INQUIRY data
if path.contains("/sr") {
// Open the sr device to get its identity
let mut sr_transport = crate::scsi::open(std::path::Path::new(path))?;
let sr_id = DriveId::from_drive(sr_transport.as_mut())?;
drop(sr_transport);
// Find matching sg device
for (sg_path, sg_id) in find_drives() {
if sg_id.vendor_id == sr_id.vendor_id
&& sg_id.product_id == sr_id.product_id
@@ -255,7 +206,6 @@ pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
}
}
// No sg match found — fall back to sr with warning
let warning = format!(
"{} is a block device (sr) — no matching sg device found, performance may be limited",
path
@@ -263,20 +213,18 @@ pub fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
return Ok((path.to_string(), Some(warning)));
}
// Unknown device type — use as-is
if !std::path::Path::new(path).exists() {
return Err(Error::DeviceNotFound { path: path.to_string() });
}
Ok((path.to_string(), None))
}
/// Create the platform-specific driver for a given chipset.
fn create_platform(profile: &DriveProfile, drive_id: &DriveId) -> Result<Box<dyn Platform>> {
match profile.chipset {
fn create_platform(chipset: Chipset, profile: &DriveProfile) -> Result<Box<dyn Platform>> {
match chipset {
Chipset::MediaTek => Ok(Box::new(Mt1959::new(profile.clone()))),
Chipset::Renesas => Err(Error::UnsupportedDrive {
vendor_id: drive_id.vendor_id.trim().to_string(),
product_id: drive_id.product_id.trim().to_string(),
vendor_id: profile.identity.vendor_id.trim().to_string(),
product_id: String::new(),
product_revision: "Renesas not yet implemented".to_string(),
}),
}