mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
fix(windows): list every optical drive without guessing device numbers
Enumeration probed only \\.\CdRom0..15 and fell back to drive letters only when that found nothing, so drives numbered above 15 (common after USB replugging) were silently missing. List every CdRomN from QueryDosDeviceW, add optical drive letters (GetDriveTypeW) not already covered by their device number, retry INQUIRY with the standard 36 bytes for strict USB bridges, and log each skipped device with its reason. Also syncs the stale Cargo.lock with the 1.8.0 manifest.
This commit is contained in:
Generated
+9
-2
@@ -221,12 +221,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "freemkv-unlock"
|
name = "freemkv-unlock"
|
||||||
version = "1.7.7"
|
version = "1.8.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"pioneer-optical",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -317,7 +318,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libfreemkv"
|
name = "libfreemkv"
|
||||||
version = "1.7.7"
|
version = "1.8.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -430,6 +431,12 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pioneer-optical"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf5074a27cc39fb664d0b994814ffe8530e3e8590aff0d104723332dbcb1abc4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
|
|||||||
+12
-2
@@ -544,8 +544,18 @@ const INQUIRY_MIN_LEN: usize = 36;
|
|||||||
/// Send INQUIRY and parse standard response fields. A reply shorter than the identity
|
/// Send INQUIRY and parse standard response fields. A reply shorter than the identity
|
||||||
/// fields is an error, never a blank identity.
|
/// fields is an error, never a blank identity.
|
||||||
pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result<InquiryResult> {
|
pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result<InquiryResult> {
|
||||||
let cdb = [SCSI_INQUIRY, 0x00, 0x00, 0x00, INQUIRY_ALLOC as u8, 0x00];
|
inquiry_alloc(scsi, INQUIRY_ALLOC)
|
||||||
let mut buf = [0u8; INQUIRY_ALLOC];
|
}
|
||||||
|
|
||||||
|
/// INQUIRY with the standard 36-byte allocation, for bridges that reject longer ones.
|
||||||
|
#[cfg_attr(not(windows), allow(dead_code))]
|
||||||
|
pub(crate) fn inquiry_standard(scsi: &mut dyn ScsiTransport) -> Result<InquiryResult> {
|
||||||
|
inquiry_alloc(scsi, INQUIRY_MIN_LEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inquiry_alloc(scsi: &mut dyn ScsiTransport, alloc: usize) -> Result<InquiryResult> {
|
||||||
|
let cdb = [SCSI_INQUIRY, 0x00, 0x00, 0x00, alloc as u8, 0x00];
|
||||||
|
let mut buf = vec![0u8; alloc];
|
||||||
let r = scsi.execute(
|
let r = scsi.execute(
|
||||||
&cdb,
|
&cdb,
|
||||||
DataDirection::FromDevice,
|
DataDirection::FromDevice,
|
||||||
|
|||||||
+145
-28
@@ -15,6 +15,13 @@ const IOCTL_SCSI_PASS_THROUGH_DIRECT: u32 = 0x4D014;
|
|||||||
/// IOCTL_STORAGE_QUERY_PROPERTY — CTL_CODE(IOCTL_STORAGE_BASE(0x2D),
|
/// IOCTL_STORAGE_QUERY_PROPERTY — CTL_CODE(IOCTL_STORAGE_BASE(0x2D),
|
||||||
/// 0x500, METHOD_BUFFERED(0), FILE_ANY_ACCESS(0)) = 0x002D1400.
|
/// 0x500, METHOD_BUFFERED(0), FILE_ANY_ACCESS(0)) = 0x002D1400.
|
||||||
const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D1400;
|
const IOCTL_STORAGE_QUERY_PROPERTY: u32 = 0x002D1400;
|
||||||
|
/// IOCTL_STORAGE_GET_DEVICE_NUMBER — CTL_CODE(0x2D, 0x420, METHOD_BUFFERED, FILE_ANY_ACCESS).
|
||||||
|
const IOCTL_STORAGE_GET_DEVICE_NUMBER: u32 = 0x002D1080;
|
||||||
|
/// FILE_DEVICE_CD_ROM, the `DeviceType` of an optical drive.
|
||||||
|
const FILE_DEVICE_CD_ROM: u32 = 0x02;
|
||||||
|
/// GetDriveTypeW result for an optical drive letter.
|
||||||
|
const DRIVE_CDROM: u32 = 5;
|
||||||
|
const ERROR_INSUFFICIENT_BUFFER: u32 = 122;
|
||||||
/// STORAGE_PROPERTY_ID::StorageAdapterProperty.
|
/// STORAGE_PROPERTY_ID::StorageAdapterProperty.
|
||||||
const STORAGE_ADAPTER_PROPERTY: u32 = 1;
|
const STORAGE_ADAPTER_PROPERTY: u32 = 1;
|
||||||
/// STORAGE_QUERY_TYPE::PropertyStandardQuery.
|
/// STORAGE_QUERY_TYPE::PropertyStandardQuery.
|
||||||
@@ -101,6 +108,15 @@ struct StorageAdapterDescriptor {
|
|||||||
BusMinorVersion: u16,
|
BusMinorVersion: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `STORAGE_DEVICE_NUMBER` (winioctl.h).
|
||||||
|
#[repr(C)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct StorageDeviceNumber {
|
||||||
|
DeviceType: u32,
|
||||||
|
DeviceNumber: u32,
|
||||||
|
PartitionNumber: u32,
|
||||||
|
}
|
||||||
|
|
||||||
// ── Windows FFI ────────────────────────────────────────────────────────────
|
// ── Windows FFI ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
unsafe extern "system" {
|
unsafe extern "system" {
|
||||||
@@ -118,6 +134,12 @@ unsafe extern "system" {
|
|||||||
|
|
||||||
fn GetLastError() -> u32;
|
fn GetLastError() -> u32;
|
||||||
|
|
||||||
|
fn QueryDosDeviceW(lpDeviceName: *const u16, lpTargetPath: *mut u16, ucchMax: u32) -> u32;
|
||||||
|
|
||||||
|
fn GetLogicalDrives() -> u32;
|
||||||
|
|
||||||
|
fn GetDriveTypeW(lpRootPathName: *const u16) -> u32;
|
||||||
|
|
||||||
fn DeviceIoControl(
|
fn DeviceIoControl(
|
||||||
hDevice: isize,
|
hDevice: isize,
|
||||||
dwIoControlCode: u32,
|
dwIoControlCode: u32,
|
||||||
@@ -215,45 +237,140 @@ impl SptiTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enumerate optical drives on Windows: probe `\\.\CdRom0..15`, falling back to
|
/// Enumerate optical drives on Windows without guessing device numbers: every
|
||||||
/// drive letters `D..Z` only if none match, INQUIRY each, and keep the optical
|
/// `CdRomN` the object manager knows (from `QueryDosDeviceW`, so any N), plus
|
||||||
/// ones (peripheral device type 0x05).
|
/// any optical drive letter whose device is not already listed. Each candidate
|
||||||
///
|
/// must answer INQUIRY as an optical peripheral (type 0x05).
|
||||||
/// Self-contained at the SCSI layer via `scsi::open` + `scsi::inquiry` (as the
|
|
||||||
/// Linux backend is), with no dependency on the `rip`-gated `drive`/`identity`
|
|
||||||
/// modules — `scsi` builds without `rip` (freemkv-firmware), where the old
|
|
||||||
/// `crate::drive::windows::find_drives()` delegation failed to compile.
|
|
||||||
pub(super) fn list_drives() -> Vec<super::DriveInfo> {
|
pub(super) fn list_drives() -> Vec<super::DriveInfo> {
|
||||||
fn probe(path: &str, out: &mut Vec<super::DriveInfo>) {
|
let names = cdrom_device_names();
|
||||||
let Ok(mut transport) = crate::scsi::open(Path::new(path)) else {
|
let listed: Vec<u32> = names.iter().filter_map(|name| cdrom_number(name)).collect();
|
||||||
return;
|
let mut candidates: Vec<String> = names
|
||||||
};
|
.into_iter()
|
||||||
let Ok(r) = super::inquiry(transport.as_mut()) else {
|
.map(|name| format!("\\\\.\\{name}"))
|
||||||
return;
|
.collect();
|
||||||
};
|
for letter in optical_drive_letters() {
|
||||||
if super::is_optical_peripheral(&r.raw) {
|
let path = format!("\\\\.\\{letter}:");
|
||||||
out.push(super::DriveInfo {
|
match device_number(&path) {
|
||||||
path: normalize_device_path(path),
|
Some(n) if listed.contains(&n) => {}
|
||||||
vendor: r.vendor_id,
|
_ => candidates.push(path),
|
||||||
model: r.model,
|
|
||||||
firmware: r.firmware,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut drives = Vec::new();
|
let mut drives = Vec::new();
|
||||||
for i in 0..16 {
|
for path in candidates {
|
||||||
probe(&format!("\\\\.\\CdRom{}", i), &mut drives);
|
match probe(&path) {
|
||||||
|
Ok(Some(drive)) => drives.push(drive),
|
||||||
|
Ok(None) => tracing::info!(path = %path, "skipping non-optical device"),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(path = %path, error = %e, "skipping optical device that did not answer")
|
||||||
}
|
}
|
||||||
// Only fall back to drive letters if the CdRom scan found nothing.
|
|
||||||
if drives.is_empty() {
|
|
||||||
for letter in b'D'..=b'Z' {
|
|
||||||
probe(&format!("{}:", letter as char), &mut drives);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drives
|
drives
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn probe(path: &str) -> Result<Option<super::DriveInfo>> {
|
||||||
|
let mut transport = crate::scsi::open(Path::new(path))?;
|
||||||
|
let r = match super::inquiry(transport.as_mut()) {
|
||||||
|
Ok(r) => r,
|
||||||
|
// Some USB bridges reject a non-standard allocation length.
|
||||||
|
Err(_) => super::inquiry_standard(transport.as_mut())?,
|
||||||
|
};
|
||||||
|
Ok(
|
||||||
|
super::is_optical_peripheral(&r.raw).then(|| super::DriveInfo {
|
||||||
|
path: normalize_device_path(path),
|
||||||
|
vendor: r.vendor_id,
|
||||||
|
model: r.model,
|
||||||
|
firmware: r.firmware,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every `CdRomN` DOS device name, sorted by N.
|
||||||
|
fn cdrom_device_names() -> Vec<String> {
|
||||||
|
let mut buf = vec![0u16; 64 * 1024];
|
||||||
|
loop {
|
||||||
|
let len = unsafe { QueryDosDeviceW(std::ptr::null(), buf.as_mut_ptr(), buf.len() as u32) };
|
||||||
|
if len != 0 {
|
||||||
|
return cdrom_names_from_multi_sz(&buf[..len as usize]);
|
||||||
|
}
|
||||||
|
if unsafe { GetLastError() } != ERROR_INSUFFICIENT_BUFFER || buf.len() >= 16 * 1024 * 1024 {
|
||||||
|
tracing::warn!("QueryDosDeviceW failed; no CdRom devices listed");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
buf.resize(buf.len() * 4, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pick the `CdRomN` names out of a `QueryDosDeviceW` multi-string.
|
||||||
|
fn cdrom_names_from_multi_sz(multi_sz: &[u16]) -> Vec<String> {
|
||||||
|
let mut names: Vec<String> = multi_sz
|
||||||
|
.split(|&c| c == 0)
|
||||||
|
.map(String::from_utf16_lossy)
|
||||||
|
.filter(|name| cdrom_number(name).is_some())
|
||||||
|
.collect();
|
||||||
|
names.sort_by_key(|name| cdrom_number(name));
|
||||||
|
names.dedup();
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cdrom_number(name: &str) -> Option<u32> {
|
||||||
|
let digits = name.strip_prefix("CdRom")?;
|
||||||
|
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
digits.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drive letters Windows reports as optical.
|
||||||
|
fn optical_drive_letters() -> Vec<char> {
|
||||||
|
let mask = unsafe { GetLogicalDrives() };
|
||||||
|
(0..26u32)
|
||||||
|
.filter(|bit| mask & (1 << bit) != 0)
|
||||||
|
.map(|bit| (b'A' + bit as u8) as char)
|
||||||
|
.filter(|letter| {
|
||||||
|
let root: Vec<u16> = format!("{letter}:\\").encode_utf16().chain([0]).collect();
|
||||||
|
unsafe { GetDriveTypeW(root.as_ptr()) == DRIVE_CDROM }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `CdRomN` number behind a device path, when Windows reports one.
|
||||||
|
fn device_number(path: &str) -> Option<u32> {
|
||||||
|
let wide: Vec<u16> = path.encode_utf16().chain([0]).collect();
|
||||||
|
// Zero access rights: querying the device number needs no read/write access.
|
||||||
|
let handle = unsafe {
|
||||||
|
CreateFileW(
|
||||||
|
wide.as_ptr(),
|
||||||
|
0,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
|
std::ptr::null(),
|
||||||
|
OPEN_EXISTING,
|
||||||
|
0,
|
||||||
|
std::ptr::null(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if handle == INVALID_HANDLE_VALUE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut number: StorageDeviceNumber = unsafe { std::mem::zeroed() };
|
||||||
|
let mut returned = 0u32;
|
||||||
|
let ok = unsafe {
|
||||||
|
DeviceIoControl(
|
||||||
|
handle,
|
||||||
|
IOCTL_STORAGE_GET_DEVICE_NUMBER,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
(&mut number as *mut StorageDeviceNumber).cast(),
|
||||||
|
std::mem::size_of::<StorageDeviceNumber>() as u32,
|
||||||
|
&mut returned,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
unsafe { CloseHandle(handle) };
|
||||||
|
(ok != 0 && number.DeviceType == FILE_DEVICE_CD_ROM).then_some(number.DeviceNumber)
|
||||||
|
}
|
||||||
|
|
||||||
/// TEST UNIT READY probe on Windows. No in-library recovery: the answer is
|
/// TEST UNIT READY probe on Windows. No in-library recovery: the answer is
|
||||||
/// classified by [`super::tur_disc_presence`].
|
/// classified by [`super::tur_disc_presence`].
|
||||||
pub(super) fn disc_presence(path: &Path) -> Result<super::DiscPresence> {
|
pub(super) fn disc_presence(path: &Path) -> Result<super::DiscPresence> {
|
||||||
|
|||||||
@@ -108,3 +108,15 @@ fn windows_ffi_constants_match_sdk() {
|
|||||||
assert_eq!(FILE_ATTRIBUTE_NORMAL, 0x80);
|
assert_eq!(FILE_ATTRIBUTE_NORMAL, 0x80);
|
||||||
assert_eq!(INVALID_HANDLE_VALUE, -1);
|
assert_eq!(INVALID_HANDLE_VALUE, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cdrom_names_come_from_the_dos_device_list_without_a_number_limit() {
|
||||||
|
let multi_sz: Vec<u16> =
|
||||||
|
"C:\0CdRom17\0PhysicalDrive0\0CdRom2\0CdRomX\0CdRom\0Volume{1}\0CdRom2\0\0"
|
||||||
|
.encode_utf16()
|
||||||
|
.collect();
|
||||||
|
assert_eq!(cdrom_names_from_multi_sz(&multi_sz), ["CdRom2", "CdRom17"]);
|
||||||
|
assert_eq!(cdrom_number("CdRom17"), Some(17));
|
||||||
|
assert_eq!(cdrom_number("CdRom"), None);
|
||||||
|
assert_eq!(cdrom_number("PhysicalDrive0"), None);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user