From e094fc66ba58175b992184d0f8732c454e8419cc Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:34:33 -0700 Subject: [PATCH] Platform-agnostic SCSI transport with cfg gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scsi::open() returns Box on any platform - Linux SG_IO behind #[cfg(target_os = "linux")] - macOS IOKit and Windows SPTI stubs with TODO - drive.rs uses scsi::open() — no platform-specific imports - Compiles on macOS (returns "platform not yet supported") --- src/bin/freemkv_info.rs | 6 ++-- src/drive.rs | 14 ++++---- src/scsi.rs | 80 ++++++++++++++++++++++++++++------------- 3 files changed, 66 insertions(+), 34 deletions(-) diff --git a/src/bin/freemkv_info.rs b/src/bin/freemkv_info.rs index 32d7041..ed32b99 100644 --- a/src/bin/freemkv_info.rs +++ b/src/bin/freemkv_info.rs @@ -42,7 +42,7 @@ fn main() { .unwrap_or("profiles"); // Open SCSI transport - let mut transport = match libfreemkv::scsi::SgIoTransport::open(device) { + let mut transport = match libfreemkv::scsi::open(device) { Ok(t) => t, Err(e) => { eprintln!("Error: Cannot open {}: {}", device.display(), e); @@ -51,7 +51,7 @@ fn main() { }; // INQUIRY - let inquiry = match libfreemkv::scsi::inquiry(&mut transport) { + let inquiry = match libfreemkv::scsi::inquiry(transport.as_mut()) { Ok(i) => i, Err(e) => { eprintln!("Error: INQUIRY failed: {}", e); @@ -60,7 +60,7 @@ fn main() { }; // GET CONFIGURATION feature 0x010C - let gc_010c = libfreemkv::scsi::get_config_010c(&mut transport).ok(); + let gc_010c = libfreemkv::scsi::get_config_010c(transport.as_mut()).ok(); if json_mode { print_json(&inquiry, &gc_010c); diff --git a/src/drive.rs b/src/drive.rs index 6736207..3770b0e 100644 --- a/src/drive.rs +++ b/src/drive.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::error::{Error, Result}; -use crate::scsi::{SgIoTransport, ScsiTransport}; +use crate::scsi::ScsiTransport; use crate::identity::DriveId; use crate::profile::{self, DriveProfile, Chipset}; use crate::platform::{Platform, DriveStatus}; @@ -26,12 +26,12 @@ 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 { - let mut transport = SgIoTransport::open(device)?; + let mut transport = crate::scsi::open(device)?; let profiles = profile::load_bundled()?; // Identify drive via standard SCSI commands // SPC-4 §6.4 (INQUIRY) + MMC-6 §5.3.10 (Feature 010Ch) - let drive_id = DriveId::from_drive(&mut transport)?; + let drive_id = DriveId::from_drive(transport.as_mut())?; // Match drive to a profile by INQUIRY fields let profile = profile::find_by_drive_id(&profiles, &drive_id) @@ -56,7 +56,7 @@ impl DriveSession { }; Ok(DriveSession { - scsi: Box::new(transport), + scsi: transport, platform, profile, drive_id, @@ -65,8 +65,8 @@ impl DriveSession { /// Open with an explicit profile (skip auto-detection). pub fn open_with_profile(device: &Path, profile: DriveProfile) -> Result { - let mut transport = SgIoTransport::open(device)?; - let drive_id = DriveId::from_drive(&mut transport)?; + let mut transport = crate::scsi::open(device)?; + let drive_id = DriveId::from_drive(transport.as_mut())?; let platform: Box = match profile.chipset { Chipset::MediaTek => { @@ -82,7 +82,7 @@ impl DriveSession { }; Ok(DriveSession { - scsi: Box::new(transport), + scsi: transport, platform, profile, drive_id, diff --git a/src/scsi.rs b/src/scsi.rs index b67fac8..95b23bd 100644 --- a/src/scsi.rs +++ b/src/scsi.rs @@ -1,4 +1,9 @@ -//! SCSI/MMC command interface via Linux SG_IO. +//! SCSI/MMC command interface. +//! +//! Platform backends: +//! - Linux: SG_IO ioctl +//! - macOS: IOKit SCSI passthrough (planned) +//! - Windows: SPTI (planned) use crate::error::{Error, Result}; use std::path::Path; @@ -17,7 +22,7 @@ pub struct ScsiResult { pub sense: [u8; 32], } -/// Low-level SCSI transport. +/// Low-level SCSI transport — implemented per platform. pub trait ScsiTransport { fn execute( &mut self, @@ -28,17 +33,18 @@ pub trait ScsiTransport { ) -> Result; } -/// Linux SG_IO transport. -pub struct SgIoTransport { - fd: i32, -} +// ─── Linux: SG_IO ─────────────────────────────────────────────────────────── -// SG_IO constants +#[cfg(target_os = "linux")] const SG_IO: libc::c_ulong = 0x2285; +#[cfg(target_os = "linux")] const SG_DXFER_NONE: i32 = -1; +#[cfg(target_os = "linux")] const SG_DXFER_TO_DEV: i32 = -2; +#[cfg(target_os = "linux")] const SG_DXFER_FROM_DEV: i32 = -3; +#[cfg(target_os = "linux")] #[repr(C)] #[allow(non_camel_case_types)] struct sg_io_hdr { @@ -66,6 +72,12 @@ struct sg_io_hdr { info: u32, } +#[cfg(target_os = "linux")] +pub struct SgIoTransport { + fd: i32, +} + +#[cfg(target_os = "linux")] impl SgIoTransport { pub fn open(device: &Path) -> Result { use std::os::unix::ffi::OsStrExt; @@ -82,12 +94,14 @@ impl SgIoTransport { } } +#[cfg(target_os = "linux")] impl Drop for SgIoTransport { fn drop(&mut self) { unsafe { libc::close(self.fd); } } } +#[cfg(target_os = "linux")] impl ScsiTransport for SgIoTransport { fn execute( &mut self, @@ -142,6 +156,31 @@ impl ScsiTransport for SgIoTransport { } } +// ─── macOS: IOKit (planned) ───────────────────────────────────────────────── + +// TODO: IOKit MMC SCSI passthrough +// Use IOSCSIPeripheralDeviceType05 (MMC device nub) +// Send SCSITaskInterface commands via IOKit user client + +// ─── Windows: SPTI (planned) ──────────────────────────────────────────────── + +// TODO: SCSI Pass Through Interface +// Use CreateFile on \\.\CdRomN +// Send IOCTL_SCSI_PASS_THROUGH_DIRECT + +// ─── Platform-agnostic open ───────────────────────────────────────────────── + +/// Open a SCSI transport for the given device path. +pub fn open(device: &Path) -> Result> { + #[cfg(target_os = "linux")] + { Ok(Box::new(SgIoTransport::open(device)?)) } + + #[cfg(not(target_os = "linux"))] + { Err(Error::DeviceNotFound { path: format!("{}: platform not yet supported (Linux only)", device.display()) }) } +} + +// ─── CDB builders (platform-agnostic) ─────────────────────────────────────── + /// SCSI INQUIRY response. #[derive(Debug, Clone)] pub struct InquiryResult { @@ -169,7 +208,7 @@ pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result { }) } -/// Send GET CONFIGURATION for feature 0x010C (drive serial number). +/// Send GET CONFIGURATION for feature 0x010C (Firmware Information). pub fn get_config_010c(scsi: &mut dyn ScsiTransport) -> Result> { let cdb = [0x46, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00]; let mut buf = [0u8; 16]; @@ -177,38 +216,31 @@ pub fn get_config_010c(scsi: &mut dyn ScsiTransport) -> Result> { Ok(buf.to_vec()) } -/// Build a READ BUFFER (0x3C) CDB with the given mode, buffer ID, offset, and length. +/// Build a READ BUFFER (0x3C) CDB. pub fn build_read_buffer(mode: u8, buffer_id: u8, offset: u32, length: u32) -> [u8; 10] { [ - 0x3C, // READ BUFFER - mode, - buffer_id, - (offset >> 16) as u8, - (offset >> 8) as u8, - offset as u8, - (length >> 16) as u8, - (length >> 8) as u8, - length as u8, + 0x3C, mode, buffer_id, + (offset >> 16) as u8, (offset >> 8) as u8, offset as u8, + (length >> 16) as u8, (length >> 8) as u8, length as u8, 0x00, ] } -/// Build a SET CD SPEED (0xBB) CDB with the given read speed. +/// Build a SET CD SPEED (0xBB) CDB. pub fn build_set_cd_speed(read_speed: u16) -> [u8; 12] { [ 0xBB, 0x00, (read_speed >> 8) as u8, read_speed as u8, - 0xFF, 0xFF, // write speed = max + 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ] } -/// Build a READ(10) CDB with the raw read flag (0x08) set. +/// Build a READ(10) CDB with the raw read flag (0x08). pub fn build_read10_raw(lba: u32, count: u16) -> [u8; 10] { [ - 0x28, 0x08, // READ(10), flag=0x08 (raw) - (lba >> 24) as u8, (lba >> 16) as u8, - (lba >> 8) as u8, lba as u8, + 0x28, 0x08, + (lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8, 0x00, (count >> 8) as u8, count as u8, 0x00,