v0.13.44: macOS raw CDB transport via IOKit exclusive access
macOS SCSI transport rewritten from hybrid MMC+pread to single-path raw CDB dispatch through SCSITaskDeviceInterface. All CDBs (INQUIRY, READ, REPORT KEY, etc.) now go through ExecuteTaskSync — 1:1 with the Linux SG_IO backend. Key changes: - New macos_shim.c: diskutil unmount → find IOBDServices → ObtainExclusiveAccess → raw CDB dispatch. Eliminates Rust-side IOKit COM vtable complexity. - build.rs compiles macos_shim.c via cc into static lib - macos.rs simplified to three FFI calls (open/close/execute) - disc/mod.rs: graduated batch restore after errors, skip-ahead through bad zones, configurable error pause
This commit is contained in:
@@ -3,5 +3,34 @@ fn main() {
|
|||||||
if target == "macos" {
|
if target == "macos" {
|
||||||
println!("cargo:rustc-link-lib=framework=IOKit");
|
println!("cargo:rustc-link-lib=framework=IOKit");
|
||||||
println!("cargo:rustc-link-lib=framework=CoreFoundation");
|
println!("cargo:rustc-link-lib=framework=CoreFoundation");
|
||||||
|
|
||||||
|
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||||
|
let obj = format!("{out_dir}/macos_shim.o");
|
||||||
|
let lib = format!("{out_dir}/libmacos_scsi.a");
|
||||||
|
|
||||||
|
std::process::Command::new("cc")
|
||||||
|
.args([
|
||||||
|
"-c",
|
||||||
|
"src/scsi/macos_shim.c",
|
||||||
|
"-o",
|
||||||
|
&obj,
|
||||||
|
"-framework",
|
||||||
|
"IOKit",
|
||||||
|
"-framework",
|
||||||
|
"CoreFoundation",
|
||||||
|
"-Wall",
|
||||||
|
"-O2",
|
||||||
|
])
|
||||||
|
.status()
|
||||||
|
.expect("failed to compile macos_shim.c");
|
||||||
|
|
||||||
|
std::process::Command::new("ar")
|
||||||
|
.args(["rcs", &lib, &obj])
|
||||||
|
.status()
|
||||||
|
.expect("failed to create static lib");
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-search=native={out_dir}");
|
||||||
|
println!("cargo:rustc-link-lib=static=macos_scsi");
|
||||||
|
println!("cargo:rerun-if-changed=src/scsi/macos_shim.c");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+88
-28
@@ -1281,6 +1281,10 @@ impl Disc {
|
|||||||
let mut buf = vec![0u8; batch as usize * 2048];
|
let mut buf = vec![0u8; batch as usize * 2048];
|
||||||
let mut bytes_done = 0u64;
|
let mut bytes_done = 0u64;
|
||||||
let mut halt_requested = false;
|
let mut halt_requested = false;
|
||||||
|
let mut current_batch = batch;
|
||||||
|
let mut consecutive_ok_since_error: u64 = 0;
|
||||||
|
let mut consecutive_errors: u64 = 0;
|
||||||
|
let mut skip_power: u32 = 0;
|
||||||
let copy_t0 = std::time::Instant::now();
|
let copy_t0 = std::time::Instant::now();
|
||||||
let mut iter_count: u64 = 0;
|
let mut iter_count: u64 = 0;
|
||||||
let mut read_ok_count: u64 = 0;
|
let mut read_ok_count: u64 = 0;
|
||||||
@@ -1333,25 +1337,11 @@ impl Disc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let block_bytes = (region_end - pos).min(batch as u64 * 2048);
|
let block_bytes = (region_end - pos).min(current_batch as u64 * 2048);
|
||||||
let block_lba = (pos / 2048) as u32;
|
let block_lba = (pos / 2048) as u32;
|
||||||
let block_count = (block_bytes / 2048) as u16;
|
let block_count = (block_bytes / 2048) as u16;
|
||||||
let recovery = !opts.skip_on_error;
|
let recovery = !opts.skip_on_error;
|
||||||
|
|
||||||
if read_ok_count + read_err_count < 3 {
|
|
||||||
tracing::info!(
|
|
||||||
target: "freemkv::disc",
|
|
||||||
block_lba,
|
|
||||||
block_count,
|
|
||||||
block_bytes,
|
|
||||||
recovery,
|
|
||||||
skip_on_error = opts.skip_on_error,
|
|
||||||
batch,
|
|
||||||
total_bytes,
|
|
||||||
"Disc::copy first reads"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let read_result = reader.read_sectors(
|
let read_result = reader.read_sectors(
|
||||||
block_lba,
|
block_lba,
|
||||||
block_count,
|
block_count,
|
||||||
@@ -1361,6 +1351,30 @@ impl Disc {
|
|||||||
|
|
||||||
if read_result.is_ok() {
|
if read_result.is_ok() {
|
||||||
read_ok_count += 1;
|
read_ok_count += 1;
|
||||||
|
consecutive_ok_since_error += 1;
|
||||||
|
consecutive_errors = 0;
|
||||||
|
skip_power = 0;
|
||||||
|
|
||||||
|
if current_batch < batch
|
||||||
|
&& consecutive_ok_since_error >= COPY_BATCH_RESTORE_STREAK
|
||||||
|
{
|
||||||
|
let next_batch = (current_batch * 2).min(batch);
|
||||||
|
tracing::info!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "batch_restore",
|
||||||
|
prev_batch = current_batch,
|
||||||
|
batch = next_batch,
|
||||||
|
lba = block_lba,
|
||||||
|
streak = consecutive_ok_since_error,
|
||||||
|
"graduated batch restore"
|
||||||
|
);
|
||||||
|
current_batch = next_batch;
|
||||||
|
if (current_batch as usize * 2048) > buf.len() {
|
||||||
|
buf.resize(current_batch as usize * 2048, 0);
|
||||||
|
}
|
||||||
|
consecutive_ok_since_error = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if opts.decrypt {
|
if opts.decrypt {
|
||||||
crate::decrypt::decrypt_sectors(
|
crate::decrypt::decrypt_sectors(
|
||||||
&mut buf[..block_bytes as usize],
|
&mut buf[..block_bytes as usize],
|
||||||
@@ -1389,6 +1403,19 @@ impl Disc {
|
|||||||
} else {
|
} else {
|
||||||
let err = read_result.err().unwrap();
|
let err = read_result.err().unwrap();
|
||||||
read_err_count += 1;
|
read_err_count += 1;
|
||||||
|
consecutive_ok_since_error = 0;
|
||||||
|
consecutive_errors += 1;
|
||||||
|
|
||||||
|
if current_batch > 1 {
|
||||||
|
current_batch = 1;
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "batch_reduce",
|
||||||
|
lba = block_lba,
|
||||||
|
prev_batch = block_count,
|
||||||
|
"dropping to single-sector reads after error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if err.is_scsi_transport_failure() {
|
if err.is_scsi_transport_failure() {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -1407,12 +1434,12 @@ impl Disc {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ECC block failed — zero-fill, mark NonTrimmed, advance.
|
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
target: "freemkv::disc",
|
target: "freemkv::disc",
|
||||||
phase = "skip_ecc_block",
|
phase = "skip_ecc_block",
|
||||||
lba = block_lba,
|
lba = block_lba,
|
||||||
sectors = block_count,
|
sectors = block_count,
|
||||||
|
consecutive_errors,
|
||||||
error = %err,
|
error = %err,
|
||||||
"ECC block failed; marking NonTrimmed"
|
"ECC block failed; marking NonTrimmed"
|
||||||
);
|
);
|
||||||
@@ -1424,6 +1451,44 @@ impl Disc {
|
|||||||
map.record(pos, block_bytes, mapfile::SectorStatus::NonTrimmed)
|
map.record(pos, block_bytes, mapfile::SectorStatus::NonTrimmed)
|
||||||
.map_err(|e| Error::IoError { source: e })?;
|
.map_err(|e| Error::IoError { source: e })?;
|
||||||
bytes_done = bytes_done.saturating_add(block_bytes);
|
bytes_done = bytes_done.saturating_add(block_bytes);
|
||||||
|
|
||||||
|
let pause_ms = opts.error_pause_ms.unwrap_or(COPY_ERROR_PAUSE_MS);
|
||||||
|
if pause_ms > 0 {
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(pause_ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
if consecutive_errors >= COPY_SKIP_THRESHOLD {
|
||||||
|
let skip_sectors =
|
||||||
|
(COPY_SKIP_BASE_SECTORS << skip_power).min(COPY_SKIP_MAX_SECTORS);
|
||||||
|
let available = region_end.saturating_sub(pos + block_bytes);
|
||||||
|
let skip_bytes = (skip_sectors as u64 * 2048).min(available);
|
||||||
|
if skip_bytes > 0 {
|
||||||
|
let skip_lba = ((pos + block_bytes) / 2048) as u32;
|
||||||
|
tracing::warn!(
|
||||||
|
target: "freemkv::disc",
|
||||||
|
phase = "skip_ahead",
|
||||||
|
from_lba = skip_lba,
|
||||||
|
skip_sectors,
|
||||||
|
skip_power,
|
||||||
|
consecutive_errors,
|
||||||
|
"skipping ahead through bad zone"
|
||||||
|
);
|
||||||
|
let skip_zero = vec![0u8; skip_bytes as usize];
|
||||||
|
file.seek(SeekFrom::Start(pos + block_bytes))
|
||||||
|
.map_err(|e| Error::IoError { source: e })?;
|
||||||
|
file.write_all(&skip_zero)
|
||||||
|
.map_err(|e| Error::IoError { source: e })?;
|
||||||
|
map.record(
|
||||||
|
pos + block_bytes,
|
||||||
|
skip_bytes,
|
||||||
|
mapfile::SectorStatus::NonTrimmed,
|
||||||
|
)
|
||||||
|
.map_err(|e| Error::IoError { source: e })?;
|
||||||
|
bytes_done = bytes_done.saturating_add(skip_bytes);
|
||||||
|
pos += skip_bytes;
|
||||||
|
skip_power = skip_power.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pos += block_bytes;
|
pos += block_bytes;
|
||||||
@@ -1490,23 +1555,12 @@ impl Disc {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct CopyOptions<'a> {
|
pub struct CopyOptions<'a> {
|
||||||
pub decrypt: bool,
|
pub decrypt: bool,
|
||||||
/// Resume from existing mapfile + ISO if present. Without this, any
|
|
||||||
/// existing mapfile is wiped and the ISO recreated.
|
|
||||||
pub resume: bool,
|
pub resume: bool,
|
||||||
/// Override the default block size in sectors. Callers should resolve
|
|
||||||
/// this with `detect_max_batch_sectors(device_path)` for live drives.
|
|
||||||
/// When `None`, falls back to 32 sectors (64 KB BD ECC block) in
|
|
||||||
/// `skip_on_error` mode or `DEFAULT_BATCH_SECTORS=60` otherwise.
|
|
||||||
pub batch_sectors: Option<u16>,
|
pub batch_sectors: Option<u16>,
|
||||||
/// Zero-fill bad blocks in the ISO, mark them NonTrimmed in the mapfile,
|
|
||||||
/// and continue. Failed ECC blocks are left for `Disc::patch` to recover.
|
|
||||||
pub skip_on_error: bool,
|
pub skip_on_error: bool,
|
||||||
/// Per-iteration progress reporter. v0.13.16 architecture: the library
|
|
||||||
/// emits a single `PassProgress` shape via the `Progress` trait;
|
|
||||||
/// consumers compute their own derived percentages / ETAs from it. No
|
|
||||||
/// more positional `(bytes_good, pos, total)` callbacks.
|
|
||||||
pub progress: Option<&'a dyn crate::progress::Progress>,
|
pub progress: Option<&'a dyn crate::progress::Progress>,
|
||||||
pub halt: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
pub halt: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||||
|
pub error_pause_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of `Disc::copy`. `complete=true` means every byte reached a terminal
|
/// Result of `Disc::copy`. `complete=true` means every byte reached a terminal
|
||||||
@@ -1812,6 +1866,12 @@ const MAX_BATCH_SECTORS: u16 = 510;
|
|||||||
const DEFAULT_BATCH_SECTORS: u16 = 60;
|
const DEFAULT_BATCH_SECTORS: u16 = 60;
|
||||||
const MIN_BATCH_SECTORS: u16 = 3;
|
const MIN_BATCH_SECTORS: u16 = 3;
|
||||||
|
|
||||||
|
const COPY_ERROR_PAUSE_MS: u64 = 2000;
|
||||||
|
const COPY_SKIP_THRESHOLD: u64 = 8;
|
||||||
|
const COPY_SKIP_BASE_SECTORS: u32 = 32;
|
||||||
|
const COPY_SKIP_MAX_SECTORS: u32 = 8192;
|
||||||
|
const COPY_BATCH_RESTORE_STREAK: u64 = 200;
|
||||||
|
|
||||||
/// Coarse damage tier for a finished or in-progress rip. Maps the
|
/// Coarse damage tier for a finished or in-progress rip. Maps the
|
||||||
/// observable signals (bad sector count + lost wallclock playback time)
|
/// observable signals (bad sector count + lost wallclock playback time)
|
||||||
/// onto a small discrete classification so UIs can render a colored badge
|
/// onto a small discrete classification so UIs can render a colored badge
|
||||||
|
|||||||
+98
-488
@@ -1,183 +1,41 @@
|
|||||||
//! macOS SCSI transport via IOKit SCSITaskDeviceInterface.
|
//! macOS SCSI transport: IOKit SCSITaskDeviceInterface with exclusive access.
|
||||||
//!
|
//!
|
||||||
//! Sends SCSI commands to optical drives through IOKit's SCSI Architecture
|
//! Single dispatch path: **all** CDBs (INQUIRY, READ, REPORT KEY, etc.) go
|
||||||
//! Model family. Accepts BSD device paths like `/dev/disk2` or `/dev/rdisk2`.
|
//! through `SCSITaskDeviceInterface::ExecuteTaskSync` — 1:1 with the Linux
|
||||||
|
//! SG_IO backend. The C shim (`macos_shim.c`) handles:
|
||||||
//!
|
//!
|
||||||
//! Requires exclusive access to the device — unmount the disc first:
|
//! 1. `diskutil unmountDisk force` so the kernel block-storage driver releases
|
||||||
//! `diskutil unmountDisk /dev/disk2`
|
//! 2. Find `IOBDServices` directly via `IOServiceMatching` (not IOMedia walk)
|
||||||
|
//! 3. Create `MMCDeviceInterface` → `SCSITaskDeviceInterface`
|
||||||
|
//! 4. `ObtainExclusiveAccess`
|
||||||
|
//! 5. Raw CDB dispatch via `CreateSCSITask` + `ExecuteTaskSync`
|
||||||
|
|
||||||
use super::{DataDirection, ScsiResult, ScsiTransport};
|
use super::{DataDirection, ScsiResult, ScsiTransport};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
// ── IOKit / CoreFoundation type aliases ─────────────────────────────────────
|
|
||||||
|
|
||||||
type CFMutableDictionaryRef = *mut std::ffi::c_void;
|
|
||||||
type IOObject = u32;
|
|
||||||
type IOReturn = i32;
|
|
||||||
type MachPort = u32;
|
|
||||||
|
|
||||||
/// Opaque COM interface pointer — `*mut *mut VTable` (double-indirect).
|
|
||||||
/// IOKit plugins use COM-style vtables: the pointer points to a pointer
|
|
||||||
/// to the function table.
|
|
||||||
type ComRef = *mut *mut std::ffi::c_void;
|
|
||||||
|
|
||||||
const K_IO_RETURN_SUCCESS: IOReturn = 0;
|
|
||||||
|
|
||||||
// SCSI data transfer directions (SCSITaskLib.h)
|
|
||||||
const K_SCSI_DATA_TRANSFER_NO_DATA: u8 = 0;
|
|
||||||
const K_SCSI_DATA_TRANSFER_FROM_TARGET: u8 = 1;
|
|
||||||
const K_SCSI_DATA_TRANSFER_TO_TARGET: u8 = 2;
|
|
||||||
|
|
||||||
// SCSI task status values
|
|
||||||
const K_SCSI_TASK_STATUS_GOOD: u8 = 0x00;
|
|
||||||
|
|
||||||
const K_MAX_CDB_SIZE: usize = 16;
|
|
||||||
const K_SENSE_DATA_SIZE: usize = 32;
|
const K_SENSE_DATA_SIZE: usize = 32;
|
||||||
|
|
||||||
// ── IOKit plugin UUIDs ──────────────────────────────────────────────────────
|
|
||||||
// From IOKit/scsi/SCSITaskLib.h
|
|
||||||
|
|
||||||
/// kIOMMCDeviceUserClientTypeID — plugin type for MMC (optical) devices.
|
|
||||||
const K_IO_MMC_DEVICE_USER_CLIENT_TYPE_ID: [u8; 16] = [
|
|
||||||
0x97, 0xAB, 0xCF, 0x5C, 0x45, 0x71, 0x11, 0xD6, 0xB6, 0xA0, 0x00, 0x30, 0x65, 0xA4, 0x7A, 0xEE,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// kIOCFPlugInInterfaceID — base IOCFPlugin interface.
|
|
||||||
const K_IO_CFPLUGIN_INTERFACE_ID: [u8; 16] = [
|
|
||||||
0xC2, 0x44, 0xE8, 0x58, 0x10, 0x9C, 0x11, 0xD4, 0x91, 0xD4, 0x00, 0x50, 0xE4, 0xC6, 0x42, 0x6F,
|
|
||||||
];
|
|
||||||
|
|
||||||
/// kIOSCSITaskDeviceInterfaceID — the interface we QueryInterface for.
|
|
||||||
const K_IO_SCSI_TASK_DEVICE_INTERFACE_ID: [u8; 16] = [
|
|
||||||
0x61, 0x3E, 0x48, 0xB0, 0x30, 0x01, 0x11, 0xD6, 0xA4, 0xC0, 0x00, 0x0A, 0x27, 0x05, 0x28, 0x61,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ── Scatter/gather element ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
struct SCSITaskSGElement {
|
|
||||||
address: u64,
|
|
||||||
length: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── External IOKit / CoreFoundation functions ───────────────────────────────
|
|
||||||
|
|
||||||
// Rust 2024: FFI blocks declaring extern fns must be `unsafe extern`.
|
|
||||||
unsafe extern "C" {
|
unsafe extern "C" {
|
||||||
fn IOMasterPort(bootstrap: u32, master: *mut MachPort) -> IOReturn;
|
fn shim_open_exclusive(bsd_name: *const u8) -> i32;
|
||||||
fn IOBSDNameMatching(
|
fn shim_close();
|
||||||
master: MachPort,
|
fn shim_execute(
|
||||||
options: u32,
|
cdb: *const u8,
|
||||||
bsd_name: *const u8,
|
cdb_len: u8,
|
||||||
) -> CFMutableDictionaryRef;
|
buf: *mut u8,
|
||||||
fn IOServiceGetMatchingService(master: MachPort, matching: CFMutableDictionaryRef) -> IOObject;
|
buf_len: u32,
|
||||||
fn IOObjectRelease(object: IOObject) -> IOReturn;
|
data_in: i32,
|
||||||
fn IORegistryEntryGetParentEntry(
|
sense_out: *mut u8,
|
||||||
entry: IOObject,
|
sense_len: u32,
|
||||||
plane: *const u8,
|
task_status_out: *mut u8,
|
||||||
parent: *mut IOObject,
|
transfer_count: *mut u64,
|
||||||
) -> IOReturn;
|
) -> i32;
|
||||||
fn IOObjectConformsTo(object: IOObject, class_name: *const u8) -> u8;
|
|
||||||
fn IOCreatePlugInInterfaceForService(
|
|
||||||
service: IOObject,
|
|
||||||
plugin_type: *const [u8; 16],
|
|
||||||
interface_type: *const [u8; 16],
|
|
||||||
the_interface: *mut ComRef,
|
|
||||||
the_score: *mut i32,
|
|
||||||
) -> IOReturn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── COM vtable helpers ──────────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// IOKit plugin interfaces use COM-style vtables. A ComRef is **vtable —
|
|
||||||
// dereferencing once gives the vtable pointer, then index into it for
|
|
||||||
// individual function pointers.
|
|
||||||
//
|
|
||||||
// All vtable indices verified against Apple open source:
|
|
||||||
// IOSCSIArchitectureModelFamily/UserClientLib/SCSITaskLib.h
|
|
||||||
|
|
||||||
/// Read a function pointer from a COM vtable at the given index.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
/// `iface` must be a valid COM interface pointer (*mut *mut c_void), and
|
|
||||||
/// `index` must be a valid vtable slot for the target type `T`.
|
|
||||||
unsafe fn vtable_fn<T>(iface: ComRef, index: usize) -> T {
|
|
||||||
// Rust 2024: `unsafe fn` bodies are no longer implicitly unsafe.
|
|
||||||
// Each unsafe op needs its own `unsafe { }` block.
|
|
||||||
unsafe {
|
|
||||||
let vtable = *iface as *const *const std::ffi::c_void;
|
|
||||||
let fn_ptr = *vtable.add(index);
|
|
||||||
std::mem::transmute_copy(&fn_ptr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call Release (vtable index 3) on any COM interface.
|
|
||||||
fn com_release(iface: ComRef) {
|
|
||||||
type Fn = unsafe extern "C" fn(ComRef) -> u32;
|
|
||||||
unsafe {
|
|
||||||
let f: Fn = vtable_fn(iface, 3);
|
|
||||||
f(iface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── SCSITaskDeviceInterface vtable ──────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Index Method
|
|
||||||
// 0 _reserved
|
|
||||||
// 1 QueryInterface
|
|
||||||
// 2 AddRef
|
|
||||||
// 3 Release
|
|
||||||
// 4 IsExclusiveAccessAvailable
|
|
||||||
// 5 AddCallbackDispatcherToRunLoop
|
|
||||||
// 6 RemoveCallbackDispatcherFromRunLoop
|
|
||||||
// 7 ObtainExclusiveAccess
|
|
||||||
// 8 ReleaseExclusiveAccess
|
|
||||||
// 9 CreateSCSITask
|
|
||||||
|
|
||||||
const VTIDX_OBTAIN_EXCLUSIVE: usize = 7;
|
|
||||||
const VTIDX_RELEASE_EXCLUSIVE: usize = 8;
|
|
||||||
const VTIDX_CREATE_TASK: usize = 9;
|
|
||||||
|
|
||||||
// ── SCSITaskInterface vtable ────────────────────────────────────────────────
|
|
||||||
//
|
|
||||||
// Index Method
|
|
||||||
// 0 _reserved
|
|
||||||
// 1 QueryInterface
|
|
||||||
// 2 AddRef
|
|
||||||
// 3 Release
|
|
||||||
// 4 IsTaskActive
|
|
||||||
// 5 SetTaskAttribute
|
|
||||||
// 6 GetTaskAttribute
|
|
||||||
// 7 GetTaskState
|
|
||||||
// 8 SetCommandDescriptorBlock
|
|
||||||
// 9 GetCommandDescriptorBlockSize
|
|
||||||
// 10 GetCommandDescriptorBlock
|
|
||||||
// 11 SetScatterGatherEntries
|
|
||||||
// 12 SetTimeoutDuration
|
|
||||||
// 13 GetTimeoutDuration
|
|
||||||
// 14 SetTaskCompletionCallback
|
|
||||||
// 15 ExecuteTaskSync
|
|
||||||
// 16 ExecuteTaskAsync
|
|
||||||
// 17 AbortTask
|
|
||||||
// 18 GetSCSIServiceResponse
|
|
||||||
// 19 GetTaskStatus
|
|
||||||
// 20 GetRealizedDataTransferCount
|
|
||||||
// 21 GetAutoSenseData
|
|
||||||
|
|
||||||
const VTIDX_SET_CDB: usize = 8;
|
|
||||||
const VTIDX_SET_SG: usize = 11;
|
|
||||||
const VTIDX_SET_TIMEOUT: usize = 12;
|
|
||||||
const VTIDX_EXECUTE_SYNC: usize = 15;
|
|
||||||
|
|
||||||
// ── Transport implementation ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub struct MacScsiTransport {
|
pub struct MacScsiTransport {
|
||||||
device_iface: ComRef,
|
_bsd_name: String,
|
||||||
exclusive: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IOKit COM interface pointers are Mach port references — safe to send between threads.
|
|
||||||
unsafe impl Send for MacScsiTransport {}
|
unsafe impl Send for MacScsiTransport {}
|
||||||
|
|
||||||
impl MacScsiTransport {
|
impl MacScsiTransport {
|
||||||
@@ -186,7 +44,6 @@ impl MacScsiTransport {
|
|||||||
path: device.display().to_string(),
|
path: device.display().to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Strip /dev/ prefix to get BSD name (e.g. "disk2")
|
|
||||||
let bsd_name = if let Some(rest) = dev_str.strip_prefix("/dev/r") {
|
let bsd_name = if let Some(rest) = dev_str.strip_prefix("/dev/r") {
|
||||||
rest
|
rest
|
||||||
} else if let Some(rest) = dev_str.strip_prefix("/dev/") {
|
} else if let Some(rest) = dev_str.strip_prefix("/dev/") {
|
||||||
@@ -195,88 +52,87 @@ impl MacScsiTransport {
|
|||||||
dev_str
|
dev_str
|
||||||
};
|
};
|
||||||
|
|
||||||
let device_iface = Self::acquire_device_iface(bsd_name)?;
|
let mut bsd_c = bsd_name.as_bytes().to_vec();
|
||||||
|
bsd_c.push(0);
|
||||||
|
|
||||||
|
let rc = unsafe { shim_open_exclusive(bsd_c.as_ptr()) };
|
||||||
|
if rc != 0 {
|
||||||
|
return Err(Error::DeviceNotFound {
|
||||||
|
path: bsd_name.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(MacScsiTransport {
|
Ok(MacScsiTransport {
|
||||||
device_iface,
|
_bsd_name: bsd_name.to_string(),
|
||||||
exclusive: true,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the BSD name → IOKit SCSITaskDeviceInterface with exclusive
|
|
||||||
/// access. Returns the COM ref the caller must release.
|
|
||||||
fn acquire_device_iface(bsd_name: &str) -> Result<ComRef> {
|
|
||||||
let service = find_scsi_service(bsd_name)?;
|
|
||||||
|
|
||||||
// Create IOKit plugin for the MMC device
|
|
||||||
let mut plugin: ComRef = std::ptr::null_mut();
|
|
||||||
let mut score: i32 = 0;
|
|
||||||
let kr = unsafe {
|
|
||||||
IOCreatePlugInInterfaceForService(
|
|
||||||
service,
|
|
||||||
&K_IO_MMC_DEVICE_USER_CLIENT_TYPE_ID,
|
|
||||||
&K_IO_CFPLUGIN_INTERFACE_ID,
|
|
||||||
&mut plugin,
|
|
||||||
&mut score,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
unsafe { IOObjectRelease(service) };
|
|
||||||
|
|
||||||
if kr != K_IO_RETURN_SUCCESS || plugin.is_null() {
|
|
||||||
return Err(Error::IoKitPluginFailed {
|
|
||||||
path: bsd_name.to_string(),
|
|
||||||
kr: kr as u32,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryInterface for SCSITaskDeviceInterface
|
|
||||||
let mut device_iface: ComRef = std::ptr::null_mut();
|
|
||||||
let hr = unsafe {
|
|
||||||
type QiFn = unsafe extern "C" fn(ComRef, *const [u8; 16], *mut ComRef) -> i32;
|
|
||||||
let qi: QiFn = vtable_fn(plugin, 1);
|
|
||||||
qi(
|
|
||||||
plugin,
|
|
||||||
&K_IO_SCSI_TASK_DEVICE_INTERFACE_ID,
|
|
||||||
&mut device_iface,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
com_release(plugin);
|
|
||||||
|
|
||||||
if hr != 0 || device_iface.is_null() {
|
|
||||||
return Err(Error::ScsiInterfaceUnavailable {
|
|
||||||
path: bsd_name.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtain exclusive access
|
|
||||||
let kr = unsafe {
|
|
||||||
type Fn = unsafe extern "C" fn(ComRef) -> IOReturn;
|
|
||||||
let f: Fn = vtable_fn(device_iface, VTIDX_OBTAIN_EXCLUSIVE);
|
|
||||||
f(device_iface)
|
|
||||||
};
|
|
||||||
if kr != K_IO_RETURN_SUCCESS {
|
|
||||||
com_release(device_iface);
|
|
||||||
return Err(Error::DeviceLocked {
|
|
||||||
path: bsd_name.to_string(),
|
|
||||||
kr: kr as u32,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(device_iface)
|
|
||||||
}
|
|
||||||
|
|
||||||
// `reset()` removed in 0.13.6 — see scsi/mod.rs for rationale.
|
|
||||||
// `try_recover()` removed in 0.13.20 — userspace handle-recovery on
|
|
||||||
// task failure was the same anti-pattern stripped from Linux SG_IO
|
|
||||||
// (see internal architecture audit, 2026-04-26).
|
|
||||||
// Errors bubble up; caller decides whether to reopen the Drive.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enumerate optical drives on macOS. Mirrors `drive::macos::find_drives`
|
impl Drop for MacScsiTransport {
|
||||||
/// (which iterates `/dev/disk0..15` + INQUIRY + filters peripheral
|
fn drop(&mut self) {
|
||||||
/// type 5). Same logic, exposed through the new `DriveInfo` shape so
|
unsafe { shim_close() };
|
||||||
/// callers — `list_drives()` in `scsi::mod` — never reach into
|
}
|
||||||
/// `crate::drive::macos`.
|
}
|
||||||
|
|
||||||
|
impl ScsiTransport for MacScsiTransport {
|
||||||
|
fn execute(
|
||||||
|
&mut self,
|
||||||
|
cdb: &[u8],
|
||||||
|
direction: DataDirection,
|
||||||
|
data: &mut [u8],
|
||||||
|
_timeout_ms: u32,
|
||||||
|
) -> Result<ScsiResult> {
|
||||||
|
let data_in = match direction {
|
||||||
|
DataDirection::FromDevice => 1,
|
||||||
|
DataDirection::ToDevice => 0,
|
||||||
|
DataDirection::None => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut sense = [0u8; K_SENSE_DATA_SIZE];
|
||||||
|
let mut task_status: u8 = 0xFF;
|
||||||
|
let mut transfer_count: u64 = 0;
|
||||||
|
|
||||||
|
let kr = unsafe {
|
||||||
|
shim_execute(
|
||||||
|
cdb.as_ptr(),
|
||||||
|
cdb.len() as u8,
|
||||||
|
data.as_mut_ptr(),
|
||||||
|
data.len() as u32,
|
||||||
|
data_in,
|
||||||
|
sense.as_mut_ptr(),
|
||||||
|
K_SENSE_DATA_SIZE as u32,
|
||||||
|
&mut task_status,
|
||||||
|
&mut transfer_count,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if kr != 0 {
|
||||||
|
return Err(Error::ScsiError {
|
||||||
|
opcode: cdb[0],
|
||||||
|
status: super::SCSI_STATUS_TRANSPORT_FAILURE,
|
||||||
|
sense: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if task_status != 0 {
|
||||||
|
let parsed = super::parse_sense(&sense, K_SENSE_DATA_SIZE as u8);
|
||||||
|
return Err(Error::ScsiError {
|
||||||
|
opcode: cdb[0],
|
||||||
|
status: task_status,
|
||||||
|
sense: Some(parsed),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ScsiResult {
|
||||||
|
status: 0,
|
||||||
|
bytes_transferred: transfer_count as usize,
|
||||||
|
sense,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Drive enumeration ────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub(super) fn list_drives() -> Vec<super::DriveInfo> {
|
pub(super) fn list_drives() -> Vec<super::DriveInfo> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for i in 0..K_DEV_DISK_MAX {
|
for i in 0..K_DEV_DISK_MAX {
|
||||||
@@ -292,7 +148,6 @@ pub(super) fn list_drives() -> Vec<super::DriveInfo> {
|
|||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
// SCSI peripheral type field is the lower 5 bits of byte 0.
|
|
||||||
if inquiry.raw.is_empty()
|
if inquiry.raw.is_empty()
|
||||||
|| (inquiry.raw[K_INQUIRY_TYPE_BYTE] & K_INQUIRY_TYPE_MASK) != K_SCSI_TYPE_OPTICAL
|
|| (inquiry.raw[K_INQUIRY_TYPE_BYTE] & K_INQUIRY_TYPE_MASK) != K_SCSI_TYPE_OPTICAL
|
||||||
{
|
{
|
||||||
@@ -308,24 +163,11 @@ pub(super) fn list_drives() -> Vec<super::DriveInfo> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximum BSD disk index probed during enumeration. macOS assigns
|
|
||||||
/// `/dev/diskN` sequentially per attached storage device; 16 covers
|
|
||||||
/// any realistic homelab.
|
|
||||||
const K_DEV_DISK_MAX: u8 = 16;
|
const K_DEV_DISK_MAX: u8 = 16;
|
||||||
|
|
||||||
/// SCSI INQUIRY response: peripheral device type lives in byte 0,
|
|
||||||
/// lower 5 bits.
|
|
||||||
const K_INQUIRY_TYPE_BYTE: usize = 0;
|
const K_INQUIRY_TYPE_BYTE: usize = 0;
|
||||||
const K_INQUIRY_TYPE_MASK: u8 = 0x1F;
|
const K_INQUIRY_TYPE_MASK: u8 = 0x1F;
|
||||||
|
|
||||||
/// SCSI peripheral type 5 = "CD-ROM device" (covers DVD, BD-ROM, BD-RE).
|
|
||||||
const K_SCSI_TYPE_OPTICAL: u8 = 0x05;
|
const K_SCSI_TYPE_OPTICAL: u8 = 0x05;
|
||||||
|
|
||||||
/// TEST UNIT READY probe on macOS. Any non-"not ready" error bubbles up
|
|
||||||
/// to the caller — in-library wedge recovery was rolled back in 0.13.4
|
|
||||||
/// after USB-layer resets failed to recover the LG BU40N on Linux; the
|
|
||||||
/// macOS impl mirrors that choice for symmetry. See the Linux
|
|
||||||
/// `drive_has_disc` in `scsi/linux.rs` for the full rationale.
|
|
||||||
pub(super) fn drive_has_disc(path: &Path) -> Result<bool> {
|
pub(super) fn drive_has_disc(path: &Path) -> Result<bool> {
|
||||||
let mut transport = MacScsiTransport::open(path)?;
|
let mut transport = MacScsiTransport::open(path)?;
|
||||||
let cdb = [crate::scsi::SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0];
|
let cdb = [crate::scsi::SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0];
|
||||||
@@ -341,235 +183,3 @@ pub(super) fn drive_has_disc(path: &Path) -> Result<bool> {
|
|||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for MacScsiTransport {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if self.device_iface.is_null() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if self.exclusive {
|
|
||||||
unsafe {
|
|
||||||
type Fn = unsafe extern "C" fn(ComRef) -> IOReturn;
|
|
||||||
let f: Fn = vtable_fn(self.device_iface, VTIDX_RELEASE_EXCLUSIVE);
|
|
||||||
f(self.device_iface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
com_release(self.device_iface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScsiTransport for MacScsiTransport {
|
|
||||||
fn execute(
|
|
||||||
&mut self,
|
|
||||||
cdb: &[u8],
|
|
||||||
direction: DataDirection,
|
|
||||||
data: &mut [u8],
|
|
||||||
timeout_ms: u32,
|
|
||||||
) -> Result<ScsiResult> {
|
|
||||||
// Create a SCSI task
|
|
||||||
let task: ComRef = unsafe {
|
|
||||||
type Fn = unsafe extern "C" fn(ComRef) -> ComRef;
|
|
||||||
let f: Fn = vtable_fn(self.device_iface, VTIDX_CREATE_TASK);
|
|
||||||
f(self.device_iface)
|
|
||||||
};
|
|
||||||
if task.is_null() {
|
|
||||||
return Err(Error::ScsiError {
|
|
||||||
opcode: cdb[0],
|
|
||||||
status: super::SCSI_STATUS_TRANSPORT_FAILURE,
|
|
||||||
sense: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set CDB
|
|
||||||
let mut cdb_padded = [0u8; K_MAX_CDB_SIZE];
|
|
||||||
let cdb_len = cdb.len().min(K_MAX_CDB_SIZE);
|
|
||||||
cdb_padded[..cdb_len].copy_from_slice(&cdb[..cdb_len]);
|
|
||||||
unsafe {
|
|
||||||
type Fn = unsafe extern "C" fn(ComRef, *const u8, u8) -> IOReturn;
|
|
||||||
let f: Fn = vtable_fn(task, VTIDX_SET_CDB);
|
|
||||||
f(task, cdb_padded.as_ptr(), cdb_len as u8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set scatter/gather and transfer direction
|
|
||||||
let iokit_dir = match direction {
|
|
||||||
DataDirection::None => K_SCSI_DATA_TRANSFER_NO_DATA,
|
|
||||||
DataDirection::FromDevice => K_SCSI_DATA_TRANSFER_FROM_TARGET,
|
|
||||||
DataDirection::ToDevice => K_SCSI_DATA_TRANSFER_TO_TARGET,
|
|
||||||
};
|
|
||||||
|
|
||||||
if direction != DataDirection::None && !data.is_empty() {
|
|
||||||
let sg = SCSITaskSGElement {
|
|
||||||
address: data.as_mut_ptr() as u64,
|
|
||||||
length: data.len() as u64,
|
|
||||||
};
|
|
||||||
unsafe {
|
|
||||||
type Fn =
|
|
||||||
unsafe extern "C" fn(ComRef, *const SCSITaskSGElement, u8, u64, u8) -> IOReturn;
|
|
||||||
let f: Fn = vtable_fn(task, VTIDX_SET_SG);
|
|
||||||
f(task, &sg, 1, data.len() as u64, iokit_dir);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unsafe {
|
|
||||||
type Fn =
|
|
||||||
unsafe extern "C" fn(ComRef, *const SCSITaskSGElement, u8, u64, u8) -> IOReturn;
|
|
||||||
let f: Fn = vtable_fn(task, VTIDX_SET_SG);
|
|
||||||
f(task, std::ptr::null(), 0, 0, K_SCSI_DATA_TRANSFER_NO_DATA);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set timeout (IOKit SCSITask takes milliseconds)
|
|
||||||
unsafe {
|
|
||||||
type Fn = unsafe extern "C" fn(ComRef, u32);
|
|
||||||
let f: Fn = vtable_fn(task, VTIDX_SET_TIMEOUT);
|
|
||||||
f(task, timeout_ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute synchronously
|
|
||||||
let mut sense = [0u8; K_SENSE_DATA_SIZE];
|
|
||||||
let mut task_status: u32 = 0;
|
|
||||||
let mut realized_count: u64 = 0;
|
|
||||||
|
|
||||||
let kr = unsafe {
|
|
||||||
type Fn = unsafe extern "C" fn(ComRef, *mut u8, *mut u32, *mut u64) -> IOReturn;
|
|
||||||
let f: Fn = vtable_fn(task, VTIDX_EXECUTE_SYNC);
|
|
||||||
f(
|
|
||||||
task,
|
|
||||||
sense.as_mut_ptr(),
|
|
||||||
&mut task_status,
|
|
||||||
&mut realized_count,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
com_release(task);
|
|
||||||
|
|
||||||
if kr != K_IO_RETURN_SUCCESS {
|
|
||||||
// Task-level failure (timeout / IOKit error). Bubble it up;
|
|
||||||
// the kernel mid-layer has already done what it can.
|
|
||||||
return Err(Error::ScsiError {
|
|
||||||
opcode: cdb[0],
|
|
||||||
status: super::SCSI_STATUS_TRANSPORT_FAILURE,
|
|
||||||
sense: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if task_status != K_SCSI_TASK_STATUS_GOOD as u32 {
|
|
||||||
// IOKit doesn't surface a "bytes written into sense buffer"
|
|
||||||
// count the way SG_IO does — pass the buffer's full length
|
|
||||||
// and let parse_sense inspect byte 0's response code to pick
|
|
||||||
// descriptor-vs-fixed format.
|
|
||||||
//
|
|
||||||
// 0.13.23: carry the full SPC-4 sense triple in
|
|
||||||
// `Error::ScsiError::sense` so callers can route on
|
|
||||||
// `ScsiSense::is_medium_error()` etc.
|
|
||||||
let parsed = super::parse_sense(&sense, sense.len() as u8);
|
|
||||||
return Err(Error::ScsiError {
|
|
||||||
opcode: cdb[0],
|
|
||||||
status: task_status as u8,
|
|
||||||
sense: Some(parsed),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ScsiResult {
|
|
||||||
status: task_status as u8,
|
|
||||||
bytes_transferred: realized_count as usize,
|
|
||||||
sense,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── IOKit service discovery ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// BSD name → IOKit service for the SCSI device.
|
|
||||||
///
|
|
||||||
/// Walk: IOMedia (BSD name match) → parent chain → SCSIPeripheralDeviceNub.
|
|
||||||
///
|
|
||||||
/// All failure paths surface as `Error::DeviceNotFound { path: bsd_name }` —
|
|
||||||
/// the four internal stages (IOMasterPort / IOBSDNameMatching / IOMedia
|
|
||||||
/// lookup / walk_to_authoring_device) collapse into one observable error
|
|
||||||
/// because none of them are user-actionable individually. Pre-0.13 each
|
|
||||||
/// stage stuffed an English description into `path:` ("…IOMasterPort
|
|
||||||
/// failed", "…SCSITaskDeviceInterface not available", etc.) which broke
|
|
||||||
/// the library's "no English text" rule.
|
|
||||||
fn find_scsi_service(bsd_name: &str) -> Result<IOObject> {
|
|
||||||
let not_found = || Error::DeviceNotFound {
|
|
||||||
path: bsd_name.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut master: MachPort = 0;
|
|
||||||
let kr = unsafe { IOMasterPort(0, &mut master) };
|
|
||||||
if kr != K_IO_RETURN_SUCCESS {
|
|
||||||
return Err(not_found());
|
|
||||||
}
|
|
||||||
|
|
||||||
// IOBSDNameMatching creates a dictionary matching { "BSD Name" = bsd_name }
|
|
||||||
let mut bsd_c = bsd_name.as_bytes().to_vec();
|
|
||||||
bsd_c.push(0);
|
|
||||||
let matching = unsafe { IOBSDNameMatching(master, 0, bsd_c.as_ptr()) };
|
|
||||||
if matching.is_null() {
|
|
||||||
return Err(not_found());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the single IOMedia service (consumes the matching dict)
|
|
||||||
let media = unsafe { IOServiceGetMatchingService(master, matching) };
|
|
||||||
if media == 0 {
|
|
||||||
return Err(not_found());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk up the IOService plane to find the authoring device.
|
|
||||||
// The chain is typically:
|
|
||||||
// IOMedia → IOPartitionScheme → IOMedia → IOBlockStorageDriver
|
|
||||||
// → IOSCSIPeripheralDeviceNub (this is what we want)
|
|
||||||
//
|
|
||||||
// We walk up until we find a service that IOCreatePlugInInterfaceForService
|
|
||||||
// accepts with kIOMMCDeviceUserClientTypeID, or until we hit the root.
|
|
||||||
let service = walk_to_authoring_device(media);
|
|
||||||
unsafe { IOObjectRelease(media) };
|
|
||||||
|
|
||||||
service.ok_or_else(not_found)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Walk up the IOService plane from an IOMedia to the SCSI authoring device.
|
|
||||||
fn walk_to_authoring_device(start: IOObject) -> Option<IOObject> {
|
|
||||||
let mut current = start;
|
|
||||||
// Retain start so we can release uniformly in the loop
|
|
||||||
// (IORegistryEntryGetParentEntry retains the parent for us)
|
|
||||||
|
|
||||||
// Target class names for authoring devices
|
|
||||||
let target_classes: &[&[u8]] = &[
|
|
||||||
b"IOSCSIPeripheralDeviceNub\0",
|
|
||||||
b"IOBDBlockStorageDevice\0",
|
|
||||||
b"IODVDBlockStorageDevice\0",
|
|
||||||
b"IOCDBlockStorageDevice\0",
|
|
||||||
b"IOBlockStorageDevice\0",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Walk up to 10 levels (more than enough)
|
|
||||||
for _ in 0..10 {
|
|
||||||
let mut parent: IOObject = 0;
|
|
||||||
let kr = unsafe {
|
|
||||||
IORegistryEntryGetParentEntry(current, c"IOService".as_ptr() as *const u8, &mut parent)
|
|
||||||
};
|
|
||||||
|
|
||||||
if current != start {
|
|
||||||
unsafe { IOObjectRelease(current) };
|
|
||||||
}
|
|
||||||
|
|
||||||
if kr != K_IO_RETURN_SUCCESS || parent == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this parent matches any of our target classes
|
|
||||||
for class in target_classes {
|
|
||||||
if unsafe { IOObjectConformsTo(parent, class.as_ptr()) } != 0 {
|
|
||||||
return Some(parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
current = parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if current != start {
|
|
||||||
unsafe { IOObjectRelease(current) };
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#include <IOKit/IOKitLib.h>
|
||||||
|
#include <IOKit/IOCFPlugIn.h>
|
||||||
|
#include <IOKit/scsi/SCSITaskLib.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
IOCFPlugInInterface **plugin;
|
||||||
|
MMCDeviceInterface **mmc;
|
||||||
|
SCSITaskDeviceInterface **scsi;
|
||||||
|
int exclusive;
|
||||||
|
} ShimHandle;
|
||||||
|
|
||||||
|
static ShimHandle g_handle = {NULL, NULL, NULL, 0};
|
||||||
|
|
||||||
|
int shim_open_exclusive(const char *bsd_name) {
|
||||||
|
kern_return_t kr;
|
||||||
|
HRESULT hr;
|
||||||
|
SInt32 score = 0;
|
||||||
|
|
||||||
|
if (g_handle.exclusive && g_handle.scsi) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
char cmd[128];
|
||||||
|
snprintf(cmd, sizeof(cmd), "diskutil unmountDisk force %s 2>/dev/null", bsd_name);
|
||||||
|
system(cmd);
|
||||||
|
usleep(500000);
|
||||||
|
|
||||||
|
mach_port_t mp;
|
||||||
|
IOMainPort(0, &mp);
|
||||||
|
|
||||||
|
CFMutableDictionaryRef matching = IOServiceMatching("IOBDServices");
|
||||||
|
io_service_t svc = IOServiceGetMatchingService(mp, matching);
|
||||||
|
if (!svc) return -1;
|
||||||
|
|
||||||
|
kr = IOCreatePlugInInterfaceForService(svc,
|
||||||
|
kIOMMCDeviceUserClientTypeID, kIOCFPlugInInterfaceID,
|
||||||
|
&g_handle.plugin, &score);
|
||||||
|
IOObjectRelease(svc);
|
||||||
|
|
||||||
|
if (kr != KERN_SUCCESS || !g_handle.plugin) return -2;
|
||||||
|
|
||||||
|
hr = (*g_handle.plugin)->QueryInterface(g_handle.plugin,
|
||||||
|
CFUUIDGetUUIDBytes(kIOMMCDeviceInterfaceID), (LPVOID *)&g_handle.mmc);
|
||||||
|
if (hr != S_OK || !g_handle.mmc) {
|
||||||
|
IODestroyPlugInInterface(g_handle.plugin);
|
||||||
|
g_handle.plugin = NULL;
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_handle.scsi = (*g_handle.mmc)->GetSCSITaskDeviceInterface(g_handle.mmc);
|
||||||
|
if (!g_handle.scsi) {
|
||||||
|
(*g_handle.mmc)->Release(g_handle.mmc);
|
||||||
|
IODestroyPlugInInterface(g_handle.plugin);
|
||||||
|
g_handle.mmc = NULL;
|
||||||
|
g_handle.plugin = NULL;
|
||||||
|
return -4;
|
||||||
|
}
|
||||||
|
|
||||||
|
kr = (*g_handle.scsi)->ObtainExclusiveAccess(g_handle.scsi);
|
||||||
|
if (kr != kIOReturnSuccess) {
|
||||||
|
(*g_handle.scsi)->Release(g_handle.scsi);
|
||||||
|
(*g_handle.mmc)->Release(g_handle.mmc);
|
||||||
|
IODestroyPlugInInterface(g_handle.plugin);
|
||||||
|
g_handle.scsi = NULL;
|
||||||
|
g_handle.mmc = NULL;
|
||||||
|
g_handle.plugin = NULL;
|
||||||
|
return -5;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_handle.exclusive = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void shim_close(void) {
|
||||||
|
if (g_handle.exclusive && g_handle.scsi) {
|
||||||
|
(*g_handle.scsi)->ReleaseExclusiveAccess(g_handle.scsi);
|
||||||
|
}
|
||||||
|
if (g_handle.scsi) {
|
||||||
|
(*g_handle.scsi)->Release(g_handle.scsi);
|
||||||
|
g_handle.scsi = NULL;
|
||||||
|
}
|
||||||
|
if (g_handle.mmc) {
|
||||||
|
(*g_handle.mmc)->Release(g_handle.mmc);
|
||||||
|
g_handle.mmc = NULL;
|
||||||
|
}
|
||||||
|
if (g_handle.plugin) {
|
||||||
|
IODestroyPlugInInterface(g_handle.plugin);
|
||||||
|
g_handle.plugin = NULL;
|
||||||
|
}
|
||||||
|
g_handle.exclusive = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int shim_execute(const unsigned char *cdb, unsigned char cdb_len,
|
||||||
|
void *buf, unsigned int buf_len, int data_in,
|
||||||
|
unsigned char *sense_out, unsigned int sense_len,
|
||||||
|
unsigned char *task_status_out, unsigned long long *transfer_count) {
|
||||||
|
if (!g_handle.scsi) return -1;
|
||||||
|
|
||||||
|
SCSITaskInterface **task = (*g_handle.scsi)->CreateSCSITask(g_handle.scsi);
|
||||||
|
if (!task) return -2;
|
||||||
|
|
||||||
|
SCSICommandDescriptorBlock cdb_buf;
|
||||||
|
memset(&cdb_buf, 0, sizeof(cdb_buf));
|
||||||
|
memcpy(&cdb_buf, cdb, cdb_len);
|
||||||
|
|
||||||
|
(*task)->SetCommandDescriptorBlock(task, cdb_buf, cdb_len);
|
||||||
|
|
||||||
|
if (buf_len > 0 && buf) {
|
||||||
|
SCSITaskSGElement sg;
|
||||||
|
sg.address = (UInt64)(uintptr_t)buf;
|
||||||
|
sg.length = buf_len;
|
||||||
|
(*task)->SetScatterGatherEntries(task, &sg, 1, buf_len,
|
||||||
|
data_in ? kSCSIDataTransfer_FromTargetToInitiator
|
||||||
|
: kSCSIDataTransfer_FromInitiatorToTarget);
|
||||||
|
} else {
|
||||||
|
(*task)->SetScatterGatherEntries(task, NULL, 0, 0,
|
||||||
|
kSCSIDataTransfer_NoDataTransfer);
|
||||||
|
}
|
||||||
|
|
||||||
|
(*task)->SetTimeoutDuration(task, 30000);
|
||||||
|
|
||||||
|
SCSI_Sense_Data sense;
|
||||||
|
memset(&sense, 0, sizeof(sense));
|
||||||
|
SCSITaskStatus status = 0xFF;
|
||||||
|
UInt64 count = 0;
|
||||||
|
|
||||||
|
IOReturn kr = (*task)->ExecuteTaskSync(task, &sense, &status, &count);
|
||||||
|
|
||||||
|
if (sense_out && sense_len > 0) {
|
||||||
|
size_t copy = sense_len < sizeof(sense) ? sense_len : sizeof(sense);
|
||||||
|
memcpy(sense_out, &sense, copy);
|
||||||
|
}
|
||||||
|
if (task_status_out) *task_status_out = (unsigned char)status;
|
||||||
|
if (transfer_count) *transfer_count = count;
|
||||||
|
|
||||||
|
(*task)->Release(task);
|
||||||
|
|
||||||
|
return (int)kr;
|
||||||
|
}
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Platform backends are in separate files:
|
//! Platform backends are in separate files:
|
||||||
//! - `linux.rs` — SG_IO ioctl
|
//! - `linux.rs` — SG_IO ioctl
|
||||||
//! - `macos.rs` — IOKit SCSITaskDeviceInterface
|
//! - `macos.rs` — IOKit SCSITaskDeviceInterface (exclusive access)
|
||||||
//! - `windows.rs` — SPTI (SCSI Pass-Through Interface)
|
//! - `windows.rs` — SPTI (SCSI Pass-Through Interface)
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|||||||
@@ -439,7 +439,7 @@ fn test_disc_copy_completes_full_disc_with_failing_reader() {
|
|||||||
let opts = CopyOptions {
|
let opts = CopyOptions {
|
||||||
decrypt: false,
|
decrypt: false,
|
||||||
skip_on_error: true,
|
skip_on_error: true,
|
||||||
|
error_pause_ms: Some(0),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -454,9 +454,7 @@ fn test_disc_copy_completes_full_disc_with_failing_reader() {
|
|||||||
let _ = std::fs::remove_file(libfreemkv::disc::mapfile_path_for(&iso_path));
|
let _ = std::fs::remove_file(libfreemkv::disc::mapfile_path_for(&iso_path));
|
||||||
|
|
||||||
// Hard bound — even at 0 ms per read, 1024 sectors with skip-forward
|
// Hard bound — even at 0 ms per read, 1024 sectors with skip-forward
|
||||||
// should complete in well under a second on any host. If this test runs
|
// should complete in well under a second on any host.
|
||||||
// for minutes, something has regressed (e.g. stall guard reintroduced
|
|
||||||
// with infinite-loop semantics, or Pass 1 is hanging on each read).
|
|
||||||
assert!(
|
assert!(
|
||||||
elapsed < Duration::from_secs(5),
|
elapsed < Duration::from_secs(5),
|
||||||
"Pass 1 took {elapsed:?} on a 2 MB synthetic disc — expected < 5 s"
|
"Pass 1 took {elapsed:?} on a 2 MB synthetic disc — expected < 5 s"
|
||||||
@@ -521,7 +519,7 @@ fn test_disc_copy_halts_promptly_on_failing_reader() {
|
|||||||
let opts = CopyOptions {
|
let opts = CopyOptions {
|
||||||
decrypt: false,
|
decrypt: false,
|
||||||
skip_on_error: true,
|
skip_on_error: true,
|
||||||
|
error_pause_ms: Some(0),
|
||||||
halt: Some(halt),
|
halt: Some(halt),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -609,6 +607,7 @@ fn test_disc_copy_marks_failed_ecc_blocks_as_nontrimmed() {
|
|||||||
let opts = CopyOptions {
|
let opts = CopyOptions {
|
||||||
decrypt: false,
|
decrypt: false,
|
||||||
skip_on_error: true,
|
skip_on_error: true,
|
||||||
|
error_pause_ms: Some(0),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -619,14 +618,20 @@ fn test_disc_copy_marks_failed_ecc_blocks_as_nontrimmed() {
|
|||||||
let _ = std::fs::remove_file(&iso_path);
|
let _ = std::fs::remove_file(&iso_path);
|
||||||
let _ = std::fs::remove_file(libfreemkv::disc::mapfile_path_for(&iso_path));
|
let _ = std::fs::remove_file(libfreemkv::disc::mapfile_path_for(&iso_path));
|
||||||
|
|
||||||
assert_eq!(
|
// With graduated batch restore, Pass 1 drops to batch=1 after the
|
||||||
result.bytes_good, 0,
|
// first batch=32 failure, reads individually (count=1 succeeds for
|
||||||
"Pass 1 should have 0 bytes_good when all batch reads fail. Got {} of {}",
|
// BlockSizeFailingReader), and recovers those sectors. After 200 OK
|
||||||
result.bytes_good, total_bytes
|
// at batch=1, it tries batch=2 which fails again. Net result: most
|
||||||
|
// sectors are recovered (Finished), only the batch>1 failures produce
|
||||||
|
// NonTrimmed blocks.
|
||||||
|
assert!(
|
||||||
|
result.bytes_good > 0,
|
||||||
|
"Pass 1 should recover batch=1-readable sectors. Got bytes_good={}",
|
||||||
|
result.bytes_good
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
result.bytes_pending > 0,
|
result.bytes_pending > 0,
|
||||||
"all sectors should be NonTrimmed pending Pass 2"
|
"batch>1 failures should produce NonTrimmed pending Pass 2"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!result.complete,
|
!result.complete,
|
||||||
|
|||||||
Reference in New Issue
Block a user