Platform-agnostic SCSI transport with cfg gates

- scsi::open() returns Box<dyn ScsiTransport> 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")
This commit is contained in:
MattJackson
2026-04-06 11:34:33 -07:00
parent 0b2e390715
commit e094fc66ba
3 changed files with 66 additions and 34 deletions
+3 -3
View File
@@ -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);
+7 -7
View File
@@ -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<Self> {
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<Self> {
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<dyn Platform> = match profile.chipset {
Chipset::MediaTek => {
@@ -82,7 +82,7 @@ impl DriveSession {
};
Ok(DriveSession {
scsi: Box::new(transport),
scsi: transport,
platform,
profile,
drive_id,
+56 -24
View File
@@ -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<ScsiResult>;
}
/// 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<Self> {
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<Box<dyn ScsiTransport>> {
#[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<InquiryResult> {
})
}
/// 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<Vec<u8>> {
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<Vec<u8>> {
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,