Files
libfreemkv/src/drive.rs
T
MattJackson cc58ab1754 AACS graceful fallback: handshake optional, disc-hash-only KEYDB path
- SCSI handshake now optional: if drive doesn't support AACS layer
 (e.g. raw-mode drives where AACS feature current=0), falls back
 to disc-hash-only KEYDB lookup
- open_no_unlock(): open drive without entering raw mode
- device_path tracking on DriveSession
- Real drive test: BU40N with Civil War UHD
 - Handshake correctly fails (05/6F/01 key not present — expected)
 - Scan continues, finds AACS directory, reads Unit_Key_RO.inf
 - KEYDB lookup attempted (disc hash matching WIP)
2026-04-07 11:41:34 -07:00

207 lines
7.6 KiB
Rust

//! 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::ScsiTransport;
use crate::identity::DriveId;
use crate::profile::{self, DriveProfile, Chipset};
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,
device_path: String,
}
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> {
eprintln!(" [dbg] opening device...");
let mut transport = crate::scsi::open(device)?;
eprintln!(" [dbg] loading profiles...");
let profiles = profile::load_bundled()?;
// Identify drive via standard SCSI commands
// SPC-4 §6.4 (INQUIRY) + MMC-6 §5.3.10 (Feature 010Ch)
eprintln!(" [dbg] identifying drive...");
let drive_id = DriveId::from_drive(transport.as_mut())?;
eprintln!(" [dbg] drive: {} {}", drive_id.vendor_id.trim(), drive_id.product_id.trim());
// Match drive to a profile by INQUIRY fields
let profile = profile::find_by_drive_id(&profiles, &drive_id)
.cloned()
.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: Box<dyn Platform> = match profile.chipset {
Chipset::MediaTek => {
Box::new(Mt1959::new(profile.clone()))
}
Chipset::Renesas => {
return Err(Error::UnsupportedDrive {
vendor_id: drive_id.vendor_id.trim().to_string(),
product_id: drive_id.product_id.trim().to_string(),
product_revision: "Renesas not yet implemented".to_string(),
});
}
};
let mut session = DriveSession {
scsi: transport,
platform,
profile,
drive_id,
device_path: device.to_string_lossy().to_string(),
};
// Always unlock on open — makes all reads work immediately.
// Silently ignore failures (unencrypted discs don't need it).
eprintln!(" [dbg] unlocking...");
let _ = session.unlock();
eprintln!(" [dbg] unlocked, session ready");
Ok(session)
}
/// Open a drive WITHOUT unlocking (raw mode).
/// Used for AACS authentication which must happen before unlock.
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()
.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: Box<dyn Platform> = match profile.chipset {
Chipset::MediaTek => Box::new(Mt1959::new(profile.clone())),
Chipset::Renesas => {
return Err(Error::UnsupportedDrive {
vendor_id: drive_id.vendor_id.trim().to_string(),
product_id: drive_id.product_id.trim().to_string(),
product_revision: "Renesas not yet implemented".to_string(),
});
}
};
Ok(DriveSession {
scsi: transport,
platform,
profile,
drive_id,
device_path: device.to_string_lossy().to_string(),
})
}
/// Device path this session was opened on.
pub fn device_path(&self) -> &str {
&self.device_path
}
/// Open with an explicit profile (skip 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: Box<dyn Platform> = match profile.chipset {
Chipset::MediaTek => {
Box::new(Mt1959::new(profile.clone()))
}
Chipset::Renesas => {
return Err(Error::UnsupportedDrive {
vendor_id: drive_id.vendor_id.trim().to_string(),
product_id: drive_id.product_id.trim().to_string(),
product_revision: "Renesas not yet implemented".to_string(),
});
}
};
Ok(DriveSession {
scsi: transport,
platform,
profile,
drive_id,
device_path: String::new(),
})
}
/// 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)
}
/// Standard READ(10) — reads disc sectors for UDF filesystem, MPLS, CLPI, etc.
/// Uses a 5-second timeout to avoid hanging on encrypted/unreadable sectors.
pub fn read_disc(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
let cdb = [
0x28, 0x00, // READ(10), no flags
(lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8,
0x00,
(count >> 8) as u8, count as u8,
0x00,
];
let result = self.scsi.as_mut().execute(
&cdb, crate::scsi::DataDirection::FromDevice, buf, 5_000)?;
Ok(result.bytes_transferred)
}
/// Send a raw SCSI CDB. Used by UDF reader and disc structure parsers.
pub fn scsi_execute(&mut self, cdb: &[u8], direction: crate::scsi::DataDirection, buf: &mut [u8], timeout_ms: u32) -> Result<crate::scsi::ScsiResult> {
self.scsi.as_mut().execute(cdb, direction, buf, timeout_ms)
}
}