//! 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, platform: Box, 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 { 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 = 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 { 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 = 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 { let mut transport = crate::scsi::open(device)?; let drive_id = DriveId::from_drive(transport.as_mut())?; let platform: Box = 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 { self.platform.status(self.scsi.as_mut()) } /// Read drive configuration block. pub fn read_config(&mut self) -> Result> { 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 { 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> { 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 { 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 { self.scsi.as_mut().execute(cdb, direction, buf, timeout_ms) } }