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
+33 -1
View File
@@ -5,6 +5,7 @@
//! | Scheme | Input | Output | Path |
//! |--------|-------|--------|------|
//! | disc:// | Yes | -- | empty (auto-detect) or /dev/sgN |
//! | disk:// | Yes | -- | alias for `disc://` (identical behavior) |
//! | iso:// | Yes | -- | file path (required) |
//! | mkv:// | Yes | Yes | file path (required) |
//! | m2ts:// | Yes | Yes | file path (required) |
@@ -91,7 +92,13 @@ impl StreamUrl {
/// Parse a URL string into a typed StreamUrl.
pub fn parse_url(url: &str) -> StreamUrl {
if let Some(rest) = url.strip_prefix("disc://") {
// `disk://` is an accepted alias for `disc://` (identical behavior):
// empty = auto-detect, path = device. Windows users commonly type
// `disk://i:` after the drive-letter convention; honor both spellings.
if let Some(rest) = url
.strip_prefix("disc://")
.or_else(|| url.strip_prefix("disk://"))
{
return if rest.is_empty() {
StreamUrl::Disc { device: None }
} else {
@@ -637,14 +644,39 @@ fn build_m2ts_pipeline<R: std::io::Read + Send + 'static>(
#[cfg(test)]
mod tests {
use super::StreamUrl;
use super::aacs_key_missing;
use super::css_key_missing;
use super::parse_url;
use super::validate_network_addr;
use super::{build_demux_state, build_iso_pipeline, input, output};
use crate::decrypt::DecryptKeys;
use crate::disc::{ContentFormat, DiscTitle, Extent};
use crate::pes::Stream as _;
use crate::sector::SectorSource;
use std::path::PathBuf;
#[test]
fn disk_scheme_is_alias_for_disc() {
// `disk://` must parse identically to `disc://`: empty = auto-detect
// (device None), a trailing path = explicit device. A Windows user
// typing `disk://i:` must reach the same live-disc path as `disc://`.
match (parse_url("disk://"), parse_url("disc://")) {
(StreamUrl::Disc { device: a }, StreamUrl::Disc { device: b }) => {
assert_eq!(a, None);
assert_eq!(b, None);
}
other => panic!("disk:// / disc:// must both be Disc, got {other:?}"),
}
match (parse_url("disk://i:"), parse_url("disc://i:")) {
(StreamUrl::Disc { device: a }, StreamUrl::Disc { device: b }) => {
assert_eq!(a, Some(PathBuf::from("i:")));
assert_eq!(b, Some(PathBuf::from("i:")));
assert_eq!(a, b, "disk:// device must match disc:// device");
}
other => panic!("disk://i: / disc://i: must both be Disc, got {other:?}"),
}
}
#[test]
fn validate_network_addr_rejects_portless() {