libfreemkv 0.5.0

Open source raw disc access library for optical drives
Documentation
//! Drive session — open, identify, unlock, 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.
//!
//! 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.

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 drive session with identification, platform, and SCSI transport.
///
/// Created via `DriveSession::open()` or `DriveSession::open_no_unlock()`.
/// All disc reading goes through this struct.
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, wait for disc, and unlock for raw reads.
    ///
    /// This is the standard entry point. After `open()`, the drive is
    /// ready for scanning and content reads.
    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 and immediately init for raw reads.
    ///
    /// Use this when you need raw disc access without AACS (e.g. capture,
    /// sector dumps). Skips AACS authentication — cannot be done after init.
    pub fn open_unlocked(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()
            .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)?;

        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,
            drive_id,
            device_path: device.to_string_lossy().to_string(),
        })
    }

    /// 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
        for _ in 0..60 {
            let mut buf = [0u8; 0];
            if self.scsi.as_mut().execute(
                &tur, crate::scsi::DataDirection::None, &mut buf, 5000
            ).is_ok() {
                return Ok(());
            }
            std::thread::sleep(std::time::Duration::from_millis(500));
        }
        Err(Error::DeviceNotFound {
            path: format!("{}: drive not ready after 30s", self.device_path),
        })
    }

    /// Device path this session was opened on.
    pub fn device_path(&self) -> &str {
        &self.device_path
    }

    /// Initialize LibreDrive — 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
    pub fn init(&mut self) -> Result<()> {
        self.platform.init(self.scsi.as_mut())
    }

    /// Check if raw disc access mode is active.
    pub fn is_unlocked(&self) -> bool {
        self.platform.is_unlocked()
    }

    /// Set read speed for a disc zone (handler 8).
    /// Called per zone change during content reads.
    pub fn set_read_speed(&mut self, lba: u32) -> Result<()> {
        self.platform.set_read_speed(self.scsi.as_mut(), lba)
    }

    /// SCSI READ(10) for disc filesystem data (UDF, MPLS, CLPI).
    pub fn read_disc(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
        let cdb = [
            crate::scsi::SCSI_READ_10, 0x00,
            (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)
    }

    /// SCSI READ(10) for m2ts content — bulk reads with longer timeout.
    pub fn read_content(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
        let cdb = [
            crate::scsi::SCSI_READ_10, 0x00,
            (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, 30_000)?;
        Ok(result.bytes_transferred)
    }

    /// 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(())
    }

    /// Execute a raw SCSI CDB. Used by parsers and AACS handshake.
    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)
    }
}

/// 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 {
        let path = format!("/dev/sg{}", i);
        if !std::path::Path::new(&path).exists() {
            continue;
        }
        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,
                };
                if profile::find_by_drive_id(&profiles, &id).is_some() {
                    drives.push((path, id));
                }
            }
        }
    }
    drives
}

/// 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() });
        }
        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
                && sg_id.serial_number == sr_id.serial_number
            {
                let warning = format!(
                    "{} is a block device (sr) — using {} (sg) for raw access",
                    path, sg_path
                );
                return Ok((sg_path, Some(warning)));
            }
        }

        // 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
        );
        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 {
        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(),
            product_revision: "Renesas not yet implemented".to_string(),
        }),
    }
}