Skip to main content

libfreemkv/drive/
mod.rs

1//! Drive session — open, identify, and read from optical drives.
2//!
3//! Three-step open:
4//!   1. `open()` — open device, identify drive. Always OEM.
5//!   2. `wait_ready()` — wait for disc to spin up. Call before reading.
6//!   3. `init()` — activate custom firmware. Removes riplock.
7//!   4. `probe_disc()` — probe disc surface. Drive learns optimal speeds.
8
9pub mod capture;
10
11// Per-platform discovery helpers (the `pub(crate)` `find_drives` /
12// equivalents). Crate-public so `scsi/{linux,macos,windows}.rs` can
13// reuse the existing enumeration logic when shaping `DriveInfo`.
14#[cfg(target_os = "linux")]
15pub(crate) mod linux;
16#[cfg(target_os = "macos")]
17pub(crate) mod macos;
18#[cfg(windows)]
19pub(crate) mod windows;
20
21use crate::error::{Error, Result};
22use crate::event::{Event, EventKind};
23use crate::identity::DriveId;
24use crate::platform::PlatformDriver;
25use crate::platform::mt1959::Mt1959;
26use crate::profile::{self, DriveProfile};
27use crate::scsi::ScsiTransport;
28use crate::sector::SectorReader;
29use std::path::Path;
30use std::sync::Arc;
31use std::sync::atomic::{AtomicBool, Ordering};
32
33/// Physical state of the drive tray and disc.
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub enum DriveStatus {
36    /// Tray is open
37    TrayOpen,
38    /// Tray closed, no disc
39    NoDisc,
40    /// Tray closed, disc present and ready
41    DiscPresent,
42    /// Drive is loading or spinning up
43    NotReady,
44    /// Could not determine status
45    Unknown,
46}
47
48// SCSI opcodes used in drive control
49const SCSI_TEST_UNIT_READY: u8 = 0x00;
50const SCSI_START_STOP_UNIT: u8 = 0x1B;
51const SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL: u8 = 0x1E;
52const SCSI_GET_EVENT_STATUS: u8 = 0x4A;
53const SCSI_MODE_SENSE: u8 = 0x5A;
54const SCSI_REPORT_KEY: u8 = 0xA4;
55
56/// Optical disc drive session -- open, identify, unlock, and read.
57pub struct Drive {
58    scsi: Box<dyn ScsiTransport>,
59    driver: Option<Box<dyn PlatformDriver>>,
60    pub profile: Option<DriveProfile>,
61    pub platform: Option<profile::Platform>,
62    pub drive_id: DriveId,
63    device_path: String,
64    /// Halt flag — when set, Drive::read() bails at the next check point.
65    halt: Arc<AtomicBool>,
66    /// Event handler — fires for read errors and library-level state changes.
67    event_fn: Option<Box<dyn Fn(Event) + Send>>,
68}
69
70impl Drive {
71    pub fn open(device: &Path) -> Result<Self> {
72        let mut transport = crate::scsi::open(device)?;
73        let profiles = profile::load_bundled()?;
74        let drive_id = DriveId::from_drive(transport.as_mut())?;
75
76        let m = profile::find_by_drive_id(&profiles, &drive_id);
77        let (driver, platform, profile) = match m {
78            Some(m) => (
79                create_driver(m.platform, &m.profile).ok(),
80                Some(m.platform),
81                Some(m.profile),
82            ),
83            None => (None, None, None),
84        };
85
86        Ok(Drive {
87            scsi: transport,
88            driver,
89            platform,
90            profile,
91            drive_id,
92            device_path: device.to_string_lossy().to_string(),
93            halt: Arc::new(AtomicBool::new(false)),
94            event_fn: None,
95        })
96    }
97
98    /// Get a clone of the halt flag. Set to true to interrupt Drive::read().
99    pub fn halt_flag(&self) -> Arc<AtomicBool> {
100        self.halt.clone()
101    }
102
103    /// Halt the drive — Drive::read() will bail at the next check point.
104    pub fn halt(&self) {
105        self.halt.store(true, Ordering::Relaxed);
106    }
107
108    /// Clear the halt flag for the next operation.
109    pub fn clear_halt(&self) {
110        self.halt.store(false, Ordering::Relaxed);
111    }
112
113    /// Set an event handler for read recovery events.
114    pub fn on_event(&mut self, f: impl Fn(Event) + Send + 'static) {
115        self.event_fn = Some(Box::new(f));
116    }
117
118    #[allow(dead_code)] // public on_event registration kept; Drive currently
119    // has no internal emission sites after the 0.13.6 recovery strip.
120    // DiscStream is the BytesRead source. Plan to drop on_event in 0.14.
121    fn emit(&self, kind: EventKind) {
122        if let Some(ref f) = self.event_fn {
123            f(Event { kind });
124        }
125    }
126
127    fn is_halted(&self) -> bool {
128        self.halt.load(Ordering::Relaxed)
129    }
130
131    /// Halt-aware SCSI execute. Returns `Err(Halted)` if the flag is set
132    /// before the command dispatches or by the time it completes. The only
133    /// path to talk to the drive in the recovery hot loop; keeps Drive::read
134    /// free of explicit halt checks.
135    fn checked_exec(
136        &mut self,
137        cdb: &[u8],
138        dir: crate::scsi::DataDirection,
139        buf: &mut [u8],
140        timeout_ms: u32,
141    ) -> Result<crate::scsi::ScsiResult> {
142        if self.is_halted() {
143            return Err(Error::Halted);
144        }
145        let r = self.scsi.as_mut().execute(cdb, dir, buf, timeout_ms)?;
146        if self.is_halted() {
147            return Err(Error::Halted);
148        }
149        Ok(r)
150    }
151
152    /// Close the drive cleanly. Unlocks tray, flushes SCSI state, closes fd.
153    /// Also runs automatically on Drop as a safety net.
154    pub fn close(self) {
155        // cleanup() runs here via Drop
156    }
157
158    /// Shared cleanup — called by Drop (and thus by close).
159    fn cleanup(&mut self) {
160        self.unlock_tray();
161    }
162
163    // NOTE: Debug aid — remove after fd issue is resolved
164    pub fn device_path_owned(&self) -> String {
165        self.device_path.clone()
166    }
167
168    /// Whether this drive has a known profile (unlock parameters available).
169    pub fn has_profile(&self) -> bool {
170        self.profile.is_some()
171    }
172
173    /// Access the SCSI transport for direct commands (used by CSS/AACS auth).
174    pub fn scsi_mut(&mut self) -> &mut dyn ScsiTransport {
175        self.scsi.as_mut()
176    }
177
178    pub fn wait_ready(&mut self) -> Result<()> {
179        let tur = [SCSI_TEST_UNIT_READY, 0x00, 0x00, 0x00, 0x00, 0x00];
180
181        for _ in 0..60 {
182            let mut buf = [0u8; 0];
183            if self
184                .scsi
185                .as_mut()
186                .execute(&tur, crate::scsi::DataDirection::None, &mut buf, 5_000)
187                .is_ok()
188            {
189                return Ok(());
190            }
191            std::thread::sleep(std::time::Duration::from_millis(500));
192        }
193        Err(Error::DeviceNotReady {
194            path: self.device_path.clone(),
195        })
196    }
197
198    /// Query the physical state of the drive — disc present, tray open, etc.
199    /// Uses GET EVENT STATUS NOTIFICATION which works regardless of firmware state.
200    pub fn drive_status(&mut self) -> DriveStatus {
201        // GET EVENT STATUS NOTIFICATION: polled, media event class (0x10)
202        let cdb = [
203            SCSI_GET_EVENT_STATUS,
204            0x01,
205            0x00,
206            0x00,
207            0x10,
208            0x00,
209            0x00,
210            0x00,
211            0x08,
212            0x00,
213        ];
214        let mut buf = [0u8; 8];
215        match self.scsi.as_mut().execute(
216            &cdb,
217            crate::scsi::DataDirection::FromDevice,
218            &mut buf,
219            5_000,
220        ) {
221            Ok(r) if r.bytes_transferred >= 6 => {
222                let media_status = buf[5];
223                // Bits 1-0: door/tray state
224                // Bit 1: media present, Bit 0: tray open
225                match media_status & 0x03 {
226                    0x00 => DriveStatus::NoDisc,      // tray closed, no disc
227                    0x01 => DriveStatus::TrayOpen,    // tray open
228                    0x02 => DriveStatus::DiscPresent, // tray closed, disc present
229                    0x03 => DriveStatus::DiscPresent, // tray closed, disc present
230                    _ => DriveStatus::Unknown,
231                }
232            }
233            _ => {
234                // Fallback: try TUR
235                let tur = [SCSI_TEST_UNIT_READY, 0x00, 0x00, 0x00, 0x00, 0x00];
236                let mut empty = [0u8; 0];
237                match self.scsi.as_mut().execute(
238                    &tur,
239                    crate::scsi::DataDirection::None,
240                    &mut empty,
241                    5_000,
242                ) {
243                    Ok(_) => DriveStatus::DiscPresent,
244                    Err(Error::ScsiError { sense_key: 2, .. }) => DriveStatus::NotReady,
245                    Err(Error::ScsiError { sense_key: 6, .. }) => DriveStatus::NotReady, // UNIT ATTENTION
246                    _ => DriveStatus::Unknown,
247                }
248            }
249        }
250    }
251
252    pub fn platform_name(&self) -> &str {
253        match self.platform {
254            Some(ref p) => p.name(),
255            None => "Unknown",
256        }
257    }
258
259    pub fn device_path(&self) -> &str {
260        &self.device_path
261    }
262
263    /// Initialize drive — unlock + firmware upload.
264    /// Optional. Adds features: removes riplock, enables UHD reads, speed control.
265    pub fn init(&mut self) -> Result<()> {
266        match self.driver {
267            Some(ref mut d) => d.init(self.scsi.as_mut()),
268            None => Err(Error::UnsupportedDrive {
269                vendor_id: self.drive_id.vendor_id.trim().to_string(),
270                product_id: self.drive_id.product_id.trim().to_string(),
271                product_revision: self.drive_id.product_revision.trim().to_string(),
272            }),
273        }
274    }
275
276    /// Probe disc surface so the drive firmware learns optimal read speeds
277    /// per region. After this the host reads at max speed and the drive
278    /// manages zones internally.
279    pub fn probe_disc(&mut self) -> Result<()> {
280        match self.driver {
281            Some(ref mut d) => d.probe_disc(self.scsi.as_mut()),
282            None => Err(Error::UnsupportedDrive {
283                vendor_id: self.drive_id.vendor_id.trim().to_string(),
284                product_id: self.drive_id.product_id.trim().to_string(),
285                product_revision: self.drive_id.product_revision.trim().to_string(),
286            }),
287        }
288    }
289
290    /// Query a specific GET CONFIGURATION feature by code.
291    /// Returns the feature data (without the 8-byte header), or None if not available.
292    pub fn get_config_feature(&mut self, feature_code: u16) -> Option<Vec<u8>> {
293        let cdb = [
294            crate::scsi::SCSI_GET_CONFIGURATION,
295            0x02,
296            (feature_code >> 8) as u8,
297            feature_code as u8,
298            0x00,
299            0x00,
300            0x00,
301            0x01,
302            0x00,
303            0x00,
304        ];
305        let mut buf = vec![0u8; 256];
306        let r = self
307            .scsi
308            .as_mut()
309            .execute(
310                &cdb,
311                crate::scsi::DataDirection::FromDevice,
312                &mut buf,
313                5_000,
314            )
315            .ok()?;
316        if r.bytes_transferred > 8 {
317            Some(buf[8..r.bytes_transferred].to_vec())
318        } else {
319            None
320        }
321    }
322
323    /// Read REPORT KEY RPC state (region playback control).
324    pub fn report_key_rpc_state(&mut self) -> Option<Vec<u8>> {
325        let cdb = [
326            SCSI_REPORT_KEY,
327            0x00,
328            0x00,
329            0x00,
330            0x00,
331            0x00,
332            0x00,
333            0x00,
334            0x00,
335            0x08,
336            0x08,
337            0x00,
338        ];
339        let mut buf = vec![0u8; 8];
340        let r = self
341            .scsi
342            .as_mut()
343            .execute(
344                &cdb,
345                crate::scsi::DataDirection::FromDevice,
346                &mut buf,
347                5_000,
348            )
349            .ok()?;
350        if r.bytes_transferred > 0 {
351            Some(buf[..r.bytes_transferred].to_vec())
352        } else {
353            None
354        }
355    }
356
357    /// Read MODE SENSE page data.
358    pub fn mode_sense_page(&mut self, page: u8) -> Option<Vec<u8>> {
359        let cdb = [
360            SCSI_MODE_SENSE,
361            0x00,
362            page,
363            0x00,
364            0x00,
365            0x00,
366            0x00,
367            0x00,
368            0xFC,
369            0x00,
370        ];
371        let mut buf = vec![0u8; 252];
372        let r = self
373            .scsi
374            .as_mut()
375            .execute(
376                &cdb,
377                crate::scsi::DataDirection::FromDevice,
378                &mut buf,
379                5_000,
380            )
381            .ok()?;
382        if r.bytes_transferred > 0 {
383            Some(buf[..r.bytes_transferred].to_vec())
384        } else {
385            None
386        }
387    }
388
389    /// Read vendor-specific READ BUFFER data.
390    pub fn read_buffer(&mut self, mode: u8, buffer_id: u8, length: u16) -> Option<Vec<u8>> {
391        let cdb = crate::scsi::build_read_buffer(mode, buffer_id, 0, length as u32);
392        let mut buf = vec![0u8; length as usize];
393        let r = self
394            .scsi
395            .as_mut()
396            .execute(
397                &cdb,
398                crate::scsi::DataDirection::FromDevice,
399                &mut buf,
400                5_000,
401            )
402            .ok()?;
403        if r.bytes_transferred > 0 {
404            Some(buf[..r.bytes_transferred].to_vec())
405        } else {
406            None
407        }
408    }
409
410    pub fn is_ready(&self) -> bool {
411        match self.driver {
412            Some(ref d) => d.is_ready(),
413            None => false,
414        }
415    }
416
417    /// Read sectors from the disc. Single-shot — no inline retries, no
418    /// SCSI reset.
419    ///
420    /// `recovery=true` bumps the per-CDB timeout to 30 s for the
421    /// `Disc::patch` pass; `recovery=false` uses 1.5 s for `Disc::copy`'s
422    /// fast skip-forward sweep. On any failure returns `Err(DiscRead)`
423    /// immediately. The orchestration layer (`Disc::patch`'s outer loop
424    /// for the patch pass, `DiscStream`'s adaptive batch halving for the
425    /// stream path) handles retries.
426    ///
427    /// Inline retry phases (5× gentle + reset+reopen + 5× more) were
428    /// removed in 0.13.6. Per
429    /// `freemkv-private/postmortems/2026-04-25-stop-wedge-and-zero-kbs.md`,
430    /// the inline reset on the LG BU40N (Initio bridge) wedged drive
431    /// firmware without ever recovering a sector. The remaining recovery
432    /// layers (Disc::patch multi-pass, DiscStream batch halving) do not
433    /// touch the wedge-prone reset path.
434    pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8], recovery: bool) -> Result<usize> {
435        let timeout_ms = if recovery { 30_000 } else { 1_500 };
436        let cdb = [
437            crate::scsi::SCSI_READ_10,
438            0x00,
439            (lba >> 24) as u8,
440            (lba >> 16) as u8,
441            (lba >> 8) as u8,
442            lba as u8,
443            0x00,
444            (count >> 8) as u8,
445            count as u8,
446            0x00,
447        ];
448
449        match self.checked_exec(
450            &cdb,
451            crate::scsi::DataDirection::FromDevice,
452            buf,
453            timeout_ms,
454        ) {
455            Ok(result) => Ok(result.bytes_transferred),
456            Err(Error::Halted) => Err(Error::Halted),
457            Err(_) => Err(Error::DiscRead { sector: lba as u64 }),
458        }
459    }
460
461    /// Read the disc capacity in sectors (2048 bytes each).
462    pub fn read_capacity(&mut self) -> Result<u32> {
463        let cdb = [
464            crate::scsi::SCSI_READ_CAPACITY,
465            0x00,
466            0x00,
467            0x00,
468            0x00,
469            0x00,
470            0x00,
471            0x00,
472            0x00,
473            0x00,
474        ];
475        let mut buf = [0u8; 8];
476        self.scsi.as_mut().execute(
477            &cdb,
478            crate::scsi::DataDirection::FromDevice,
479            &mut buf,
480            5_000,
481        )?;
482        let last_lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
483        Ok(last_lba + 1)
484    }
485
486    pub fn set_speed(&mut self, speed_kbs: u16) {
487        let cdb = crate::scsi::build_set_cd_speed(speed_kbs);
488        let mut dummy = [0u8; 0];
489        let _ = self.scsi_execute(&cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000);
490    }
491
492    /// Lock the tray so the disc cannot be ejected during a rip.
493    pub fn lock_tray(&mut self) {
494        let prevent = [
495            SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL,
496            0x00,
497            0x00,
498            0x00,
499            0x01,
500            0x00,
501        ];
502        let mut buf = [0u8; 0];
503        let _ =
504            self.scsi
505                .as_mut()
506                .execute(&prevent, crate::scsi::DataDirection::None, &mut buf, 5_000);
507    }
508
509    /// Unlock the tray so the user can manually eject the disc.
510    pub fn unlock_tray(&mut self) {
511        let allow = [
512            SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL,
513            0x00,
514            0x00,
515            0x00,
516            0x00,
517            0x00,
518        ];
519        let mut buf = [0u8; 0];
520        let _ =
521            self.scsi
522                .as_mut()
523                .execute(&allow, crate::scsi::DataDirection::None, &mut buf, 5_000);
524    }
525
526    /// Eject the disc tray. Unlocks first, then ejects.
527    pub fn eject(&mut self) -> Result<()> {
528        self.unlock_tray();
529        let eject_cdb = [SCSI_START_STOP_UNIT, 0, 0, 0, 0x02, 0];
530        let mut buf = [0u8; 0];
531        self.scsi.as_mut().execute(
532            &eject_cdb,
533            crate::scsi::DataDirection::None,
534            &mut buf,
535            30_000,
536        )?;
537        Ok(())
538    }
539
540    pub fn scsi_execute(
541        &mut self,
542        cdb: &[u8],
543        direction: crate::scsi::DataDirection,
544        buf: &mut [u8],
545        timeout_ms: u32,
546    ) -> Result<crate::scsi::ScsiResult> {
547        self.scsi.as_mut().execute(cdb, direction, buf, timeout_ms)
548    }
549}
550
551impl Drop for Drive {
552    fn drop(&mut self) {
553        self.cleanup();
554        // SgIoTransport::drop() runs next, calling libc::close(fd)
555    }
556}
557
558impl SectorReader for Drive {
559    fn read_sectors(
560        &mut self,
561        lba: u32,
562        count: u16,
563        buf: &mut [u8],
564        recovery: bool,
565    ) -> Result<usize> {
566        self.read(lba, count, buf, recovery)
567    }
568}
569
570/// Find the first optical drive on this system and open it.
571///
572/// For just listing drives without opening (e.g. UI sidebar), use
573/// `scsi::list_drives()` — that returns `DriveInfo` (path + identity)
574/// without the cost of running every drive's profile + identity probe.
575pub fn find_drive() -> Option<Drive> {
576    discover_drives()
577        .into_iter()
578        .find_map(|(path, _)| Drive::open(std::path::Path::new(&path)).ok())
579}
580
581/// Halt-aware sleep primitive — wakes within ~100 ms of `halt` flipping
582/// to true. Kept for the unit tests that cover the slicing behaviour;
583/// production code paths no longer sleep on the recovery hot path
584/// (recovery loop removed in 0.13.6).
585#[cfg(test)]
586fn sleep_until_halted(halt: &AtomicBool, total: std::time::Duration) -> Result<()> {
587    const SLICE: std::time::Duration = std::time::Duration::from_millis(100);
588    let deadline = std::time::Instant::now() + total;
589    loop {
590        if halt.load(Ordering::Relaxed) {
591            return Err(Error::Halted);
592        }
593        let now = std::time::Instant::now();
594        if now >= deadline {
595            return Ok(());
596        }
597        let remaining = deadline - now;
598        std::thread::sleep(remaining.min(SLICE));
599    }
600}
601
602/// Internal: discover drive paths + IDs without opening full Drive objects.
603fn discover_drives() -> Vec<(String, DriveId)> {
604    #[cfg(target_os = "linux")]
605    {
606        linux::find_drives()
607    }
608    #[cfg(target_os = "macos")]
609    {
610        macos::find_drives()
611    }
612    #[cfg(windows)]
613    {
614        windows::find_drives()
615    }
616}
617
618/// Resolve a device path to its raw SCSI device, with optional warning message.
619#[allow(dead_code)]
620pub(crate) fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
621    #[cfg(target_os = "linux")]
622    {
623        linux::resolve_device(path)
624    }
625    #[cfg(target_os = "macos")]
626    {
627        macos::resolve_device(path)
628    }
629    #[cfg(windows)]
630    {
631        windows::resolve_device(path)
632    }
633}
634
635fn create_driver(
636    platform: profile::Platform,
637    profile: &DriveProfile,
638) -> Result<Box<dyn PlatformDriver>> {
639    match platform {
640        profile::Platform::Mt1959A => Ok(Box::new(Mt1959::new(profile.clone(), false))),
641        profile::Platform::Mt1959B => Ok(Box::new(Mt1959::new(profile.clone(), true))),
642        profile::Platform::Renesas => Err(Error::PlatformNotImplemented {
643            platform: "renesas".to_string(),
644        }),
645    }
646}
647
648#[cfg(test)]
649mod halt_tests {
650    use super::*;
651    use std::time::{Duration, Instant};
652
653    #[test]
654    fn sleep_until_halted_completes_when_not_halted() {
655        let flag = AtomicBool::new(false);
656        let t0 = Instant::now();
657        let r = sleep_until_halted(&flag, Duration::from_millis(150));
658        assert!(r.is_ok());
659        assert!(t0.elapsed() >= Duration::from_millis(140));
660    }
661
662    #[test]
663    fn sleep_until_halted_returns_immediately_if_preflagged() {
664        let flag = AtomicBool::new(true);
665        let t0 = Instant::now();
666        let r = sleep_until_halted(&flag, Duration::from_secs(10));
667        assert!(matches!(r, Err(Error::Halted)));
668        // Must wake within one slice (100 ms) — the whole point of the
669        // primitive is that a 30 s sleep doesn't block Stop.
670        assert!(t0.elapsed() < Duration::from_millis(200));
671    }
672
673    #[test]
674    fn sleep_until_halted_wakes_mid_sleep() {
675        let flag = Arc::new(AtomicBool::new(false));
676        let f2 = flag.clone();
677        let t0 = Instant::now();
678        std::thread::spawn(move || {
679            std::thread::sleep(Duration::from_millis(150));
680            f2.store(true, Ordering::Relaxed);
681        });
682        let r = sleep_until_halted(&flag, Duration::from_secs(10));
683        assert!(matches!(r, Err(Error::Halted)));
684        let waited = t0.elapsed();
685        // Flag flipped at ~150 ms; we wake within one 100 ms slice → <300 ms.
686        assert!(waited < Duration::from_millis(350), "waited {waited:?}");
687        assert!(waited >= Duration::from_millis(140), "waited {waited:?}");
688    }
689
690    #[test]
691    fn sleep_until_halted_zero_duration_is_noop_when_not_halted() {
692        let flag = AtomicBool::new(false);
693        let r = sleep_until_halted(&flag, Duration::ZERO);
694        assert!(r.is_ok());
695    }
696}