Fix Windows multi-drive selection, disk:// alias, and READ chunking

Three fixes for a Windows ASUS Blu-ray drive that failed/spammed errors:

- resolve.rs: accept disk:// as an alias for disc:// (identical behavior;
  empty = auto-detect, path = device). Windows users commonly type
  disk://i: after the drive-letter convention.

- drive::find_drive: prefer a drive that reports media present. Enumerate
  all optical drives, query Drive::drive_status() (GET EVENT STATUS, works
  regardless of firmware), and return the first reporting DiscPresent;
  fall back to the first enumerated drive when none report a disc so
  single-drive / quirky setups don't regress. Selection policy split into
  select_drive_with_media() for unit testing.

- READ chunking: add ScsiTransport::max_transfer_bytes() (default 1 MiB).
  Windows SPTI overrides it with the adapter MaximumTransferLength queried
  via IOCTL_STORAGE_QUERY_PROPERTY / StorageAdapterProperty, clamped to a
  64 KiB floor (fallback on query failure). Drive::read now caps each
  READ(10) to that limit: small reads take the unchanged single-CDB path,
  larger reads loop over read_one() chunks, reporting the failing chunk's
  LBA on error. This stops the 16 MiB single read that exceeded the
  adapter limit, made DeviceIoControl fail, and spammed transport-failure
  warnings with slow tiny-read fallbacks.

Tests added for the disk:// alias, media-preference selection, and READ
chunk decomposition / per-chunk error LBA.
This commit is contained in:
Matthew Jackson
2026-06-22 15:02:24 -07:00
parent 4f606ae9a3
commit c73a3dbcb6
4 changed files with 411 additions and 6 deletions
+17
View File
@@ -319,6 +319,23 @@ pub trait ScsiTransport: Send {
data: &mut [u8],
timeout_ms: u32,
) -> Result<ScsiResult>;
/// Maximum number of bytes the transport can carry in a single SCSI
/// data-in transfer. A READ that requests more than this must be split
/// into chunks by the caller ([`crate::Drive::read`]) — otherwise the
/// transport fails the whole command.
///
/// The default is a conservative 1 MiB, safe on every platform. The
/// Windows backend overrides this with the adapter's real
/// `MaximumTransferLength` (queried via `IOCTL_STORAGE_QUERY_PROPERTY`):
/// a 16 MiB READ that exceeds the adapter limit makes
/// `DeviceIoControl` fail outright, which freemkv then mis-reads as a
/// transport failure and falls back to slow, log-spamming tiny reads.
/// Chunking to this limit fixes that. Linux/macOS keep the 1 MiB
/// default (well within any real `max_sectors_kb`).
fn max_transfer_bytes(&self) -> usize {
1 << 20
}
}
// ── Platform-agnostic open / reset ──────────────────────────────────────────
+105 -1
View File
@@ -12,6 +12,19 @@ use std::path::Path;
// ── Windows constants ──────────────────────────────────────────────────────
const IOCTL_SCSI_PASS_THROUGH_DIRECT: u32 = 0x4D014;
/// IOCTL_STORAGE_QUERY_PROPERTY — CTL_CODE(IOCTL_STORAGE_BASE(0x2D),
/// 0x500, METHOD_BUFFERED(0), FILE_ANY_ACCESS(0)) = 0x002D1400.
const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D1400;
/// STORAGE_PROPERTY_ID::StorageAdapterProperty.
const STORAGE_ADAPTER_PROPERTY: u32 = 1;
/// STORAGE_QUERY_TYPE::PropertyStandardQuery.
const PROPERTY_STANDARD_QUERY: u32 = 0;
/// Conservative fallback when the adapter MaximumTransferLength query
/// fails — 64 KiB is universally safe for SPTD on any Windows storage
/// stack. Also the floor we clamp a reported value up to.
const WINDOWS_MIN_TRANSFER_BYTES: usize = 64 * 1024;
const SCSI_IOCTL_DATA_OUT: u8 = 0;
const SCSI_IOCTL_DATA_IN: u8 = 1;
const SCSI_IOCTL_DATA_UNSPECIFIED: u8 = 2;
@@ -54,6 +67,43 @@ struct SptwbDirect {
sense: [u8; K_SENSE_SIZE],
}
// ── STORAGE_QUERY_PROPERTY structures (winioctl.h) ─────────────────────────
/// Input to IOCTL_STORAGE_QUERY_PROPERTY. Mirrors `STORAGE_PROPERTY_QUERY`:
/// `{ PropertyId: u32, QueryType: u32, AdditionalParameters: [u8; 1] }`.
#[repr(C)]
#[allow(non_snake_case)]
struct StoragePropertyQuery {
PropertyId: u32,
QueryType: u32,
AdditionalParameters: [u8; 1],
}
/// Subset of `STORAGE_ADAPTER_DESCRIPTOR` (winioctl.h) up to and including
/// `MaximumTransferLength`. The real struct has more trailing fields, but
/// the driver fills the whole thing and we only read this prefix; reading a
/// truncated descriptor is the documented usage. Field layout (all the
/// leading fields are present so the offset of `MaximumTransferLength` is
/// correct):
/// Version, Size, MaximumTransferLength, MaximumPhysicalPages,
/// AlignmentMask: u32 …
#[repr(C)]
#[allow(non_snake_case)]
struct StorageAdapterDescriptor {
Version: u32,
Size: u32,
MaximumTransferLength: u32,
MaximumPhysicalPages: u32,
AlignmentMask: u32,
AdapterUsesPio: u8,
AdapterScansDown: u8,
CommandQueueing: u8,
AcceleratedTransfer: u8,
BusType: u8,
BusMajorVersion: u16,
BusMinorVersion: u16,
}
// ── Windows FFI ────────────────────────────────────────────────────────────
unsafe extern "system" {
@@ -85,6 +135,11 @@ unsafe extern "system" {
pub struct SptiTransport {
handle: isize,
/// Adapter MaximumTransferLength in bytes, queried once at open via
/// IOCTL_STORAGE_QUERY_PROPERTY and clamped to at least
/// [`WINDOWS_MIN_TRANSFER_BYTES`]. A single READ larger than this fails
/// `DeviceIoControl` outright, so [`crate::Drive::read`] chunks to it.
max_transfer: usize,
}
// SptiTransport's only field is an isize HANDLE, so the compiler
@@ -147,7 +202,12 @@ impl SptiTransport {
});
}
Ok(SptiTransport { handle })
let max_transfer = query_max_transfer_bytes(handle);
Ok(SptiTransport {
handle,
max_transfer,
})
}
/// Reset the drive to a known good state.
@@ -243,7 +303,51 @@ impl Drop for SptiTransport {
}
}
/// Query the storage adapter's `MaximumTransferLength` (bytes) via
/// IOCTL_STORAGE_QUERY_PROPERTY / StorageAdapterProperty. On any failure
/// (IOCTL failed, short reply, or a nonsensical zero) returns the
/// conservative [`WINDOWS_MIN_TRANSFER_BYTES`]; otherwise clamps the
/// reported value up to that floor. Never returns 0.
fn query_max_transfer_bytes(handle: isize) -> usize {
if handle == INVALID_HANDLE_VALUE {
return WINDOWS_MIN_TRANSFER_BYTES;
}
let query = StoragePropertyQuery {
PropertyId: STORAGE_ADAPTER_PROPERTY,
QueryType: PROPERTY_STANDARD_QUERY,
AdditionalParameters: [0u8; 1],
};
let mut desc: StorageAdapterDescriptor = unsafe { std::mem::zeroed() };
let mut bytes_returned: u32 = 0;
let ok = unsafe {
DeviceIoControl(
handle,
IOCTL_STORAGE_QUERY_PROPERTY,
&query as *const _ as *mut std::ffi::c_void,
std::mem::size_of::<StoragePropertyQuery>() as u32,
&mut desc as *mut _ as *mut std::ffi::c_void,
std::mem::size_of::<StorageAdapterDescriptor>() as u32,
&mut bytes_returned,
std::ptr::null_mut(),
)
};
// MaximumTransferLength sits at offset 8; need at least that many bytes
// written for the field to be valid.
let valid = ok != 0
&& bytes_returned as usize
>= std::mem::offset_of!(StorageAdapterDescriptor, MaximumTransferLength)
+ std::mem::size_of::<u32>();
if !valid || desc.MaximumTransferLength == 0 {
return WINDOWS_MIN_TRANSFER_BYTES;
}
(desc.MaximumTransferLength as usize).max(WINDOWS_MIN_TRANSFER_BYTES)
}
impl ScsiTransport for SptiTransport {
fn max_transfer_bytes(&self) -> usize {
self.max_transfer
}
fn execute(
&mut self,
cdb: &[u8],