diff --git a/src/drive/capture.rs b/src/drive/capture.rs new file mode 100644 index 0000000..f3d98f3 --- /dev/null +++ b/src/drive/capture.rs @@ -0,0 +1,103 @@ +//! Drive data capture — read hardware information via SCSI. + +use crate::drive::DriveSession; +use crate::error::Result; + +/// Raw data captured from a drive's SCSI responses. +#[derive(Debug, Clone)] +pub struct DriveCapture { + /// Raw INQUIRY response (96 bytes) + pub inquiry: Vec, + /// Raw GET_CONFIG 010C response + pub gc_010c: Vec, + /// GET_CONFIG feature responses: (feature_code, feature_name, data) + pub features: Vec, + /// REPORT_KEY RPC state + pub rpc_state: Option>, + /// MODE SENSE page 2A (capabilities) + pub mode_2a: Option>, + /// READ_BUFFER 0xF1 (Pioneer vendor data) + pub rb_f1: Option>, + /// READ_BUFFER mode 6 (MTK vendor data) + pub rb_mode6: Option>, +} + +#[derive(Debug, Clone)] +pub struct CapturedFeature { + pub code: u16, + pub name: &'static str, + pub data: Vec, +} + +/// Feature codes to capture. +const FEATURES: &[(u16, &str)] = &[ + (0x0000, "Profile List"), + (0x0001, "Core"), + (0x0003, "Removable Medium"), + (0x0010, "Random Readable"), + (0x001D, "Multi-Read"), + (0x001E, "CD Read"), + (0x001F, "DVD Read"), + (0x0040, "BD Read"), + (0x0041, "BD Write"), + (0x0100, "Power Management"), + (0x0102, "Embedded Changer"), + (0x0107, "Real Time Streaming"), + (0x0108, "Serial Number"), + (0x010C, "Firmware Information"), + (0x010D, "AACS"), +]; + +/// Capture all available drive data via SCSI commands. +/// Returns raw responses — no formatting, no zipping, no presentation. +pub fn capture_drive_data(session: &mut DriveSession) -> Result { + let id = &session.drive_id; + + // Already have INQUIRY from drive open + let inquiry = id.raw_inquiry.clone(); + let gc_010c = id.raw_gc_010c.clone(); + + // Capture GET_CONFIG features using DriveSession's query methods + let mut features = Vec::new(); + for &(code, name) in FEATURES { + if let Some(data) = session.get_config_feature(code) { + features.push(CapturedFeature { code, name, data }); + } + } + + // Vendor-specific READ_BUFFER queries + let rb_f1 = session.read_buffer(0x02, 0xF1, 48); // Pioneer + let rb_mode6 = session.read_buffer(0x06, 0x00, 32); // MTK + + // Standard queries + let rpc_state = session.report_key_rpc_state(); + let mode_2a = session.mode_sense_page(0x2A); + + Ok(DriveCapture { + inquiry, + gc_010c, + features, + rpc_state, + mode_2a, + rb_f1, + rb_mode6, + }) +} + +/// Mask a string for privacy (letters->A, digits->0). +pub fn mask_string(s: &str) -> String { + s.chars().map(|c| { + if c.is_ascii_alphabetic() { 'A' } + else if c.is_ascii_digit() { '0' } + else { c } + }).collect() +} + +/// Mask bytes for privacy. +pub fn mask_bytes(data: &[u8]) -> Vec { + data.iter().map(|&b| { + if b.is_ascii_alphabetic() { b'A' } + else if b.is_ascii_digit() { b'0' } + else { b } + }).collect() +} diff --git a/src/drive/mod.rs b/src/drive/mod.rs index 1f18fed..6d22331 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -6,6 +6,8 @@ //! 3. `init()` — activate custom firmware. Removes riplock. //! 4. `probe_disc()` — probe disc surface. Drive learns optimal speeds. +pub mod capture; + #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] @@ -119,6 +121,51 @@ impl DriveSession { } } + /// Query a specific GET CONFIGURATION feature by code. + /// Returns the feature data (without the 8-byte header), or None if not available. + pub fn get_config_feature(&mut self, feature_code: u16) -> Option> { + let cdb = [ + crate::scsi::SCSI_GET_CONFIGURATION, 0x02, + (feature_code >> 8) as u8, feature_code as u8, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + ]; + let mut buf = vec![0u8; 256]; + let r = self.scsi.as_mut() + .execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?; + if r.bytes_transferred > 8 { + Some(buf[8..r.bytes_transferred].to_vec()) + } else { + None + } + } + + /// Read REPORT KEY RPC state (region playback control). + pub fn report_key_rpc_state(&mut self) -> Option> { + let cdb = [0xA4u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00]; + let mut buf = vec![0u8; 8]; + let r = self.scsi.as_mut() + .execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?; + if r.bytes_transferred > 0 { Some(buf[..r.bytes_transferred].to_vec()) } else { None } + } + + /// Read MODE SENSE page data. + pub fn mode_sense_page(&mut self, page: u8) -> Option> { + let cdb = [0x5Au8, 0x00, page, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00]; + let mut buf = vec![0u8; 252]; + let r = self.scsi.as_mut() + .execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?; + if r.bytes_transferred > 0 { Some(buf[..r.bytes_transferred].to_vec()) } else { None } + } + + /// Read vendor-specific READ BUFFER data. + pub fn read_buffer(&mut self, mode: u8, buffer_id: u8, length: u16) -> Option> { + let cdb = crate::scsi::build_read_buffer(mode, buffer_id, 0, length as u32); + let mut buf = vec![0u8; length as usize]; + let r = self.scsi.as_mut() + .execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?; + if r.bytes_transferred > 0 { Some(buf[..r.bytes_transferred].to_vec()) } else { None } + } + pub fn is_ready(&self) -> bool { match self.driver { Some(ref d) => d.is_ready(), diff --git a/src/lib.rs b/src/lib.rs index ee6a15b..59f201c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,6 +88,7 @@ pub(crate) mod speed; pub(crate) mod udf; pub use drive::{find_drive, find_drives, resolve_device, DriveSession}; +pub use drive::capture::{DriveCapture, CapturedFeature, capture_drive_data, mask_string, mask_bytes}; pub use error::{Error, Result}; pub use event::{Event, EventKind}; pub use identity::DriveId;