Drive recovery, reset on open, simplified DiscStream
- SgIoTransport::reset() — open/close/TUR/escalate on every open - Drive::read() — single read method with error recovery (min speed, sleep 30s, retry, phase 1/2/3 escalation) - Removed read_timeout, read_sectors, read_range — one read() method - DiscStream simplified — no on_error/on_success/Recovery, delegates all error handling to Drive::read() - IsoStream no longer decrypts — streams return raw bytes, pipeline handles decryption - reset() on all platforms (Linux real, Windows/macOS stubs) - Watchdog thread removed — kernel handles USB timeouts
This commit is contained in:
+243
-50
@@ -39,6 +39,9 @@ pub enum DriveStatus {
|
|||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recovery state after a read error — stay at min speed for N bytes.
|
||||||
|
const RECOVERY_WINDOW: u64 = 500 * 1024 * 1024; // 500 MB
|
||||||
|
|
||||||
/// Optical disc drive session -- open, identify, unlock, and read.
|
/// Optical disc drive session -- open, identify, unlock, and read.
|
||||||
pub struct Drive {
|
pub struct Drive {
|
||||||
scsi: Box<dyn ScsiTransport>,
|
scsi: Box<dyn ScsiTransport>,
|
||||||
@@ -47,6 +50,9 @@ pub struct Drive {
|
|||||||
pub platform: Option<profile::Platform>,
|
pub platform: Option<profile::Platform>,
|
||||||
pub drive_id: DriveId,
|
pub drive_id: DriveId,
|
||||||
device_path: String,
|
device_path: String,
|
||||||
|
/// Bytes remaining in the min-speed recovery window.
|
||||||
|
/// After a read error, we stay at min speed for RECOVERY_WINDOW bytes.
|
||||||
|
recovery_bytes_remaining: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drive {
|
impl Drive {
|
||||||
@@ -72,9 +78,26 @@ impl Drive {
|
|||||||
profile,
|
profile,
|
||||||
drive_id,
|
drive_id,
|
||||||
device_path: device.to_string_lossy().to_string(),
|
device_path: device.to_string_lossy().to_string(),
|
||||||
|
recovery_bytes_remaining: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Close the drive cleanly. Unlocks tray, flushes SCSI state, closes fd.
|
||||||
|
/// Also runs automatically on Drop as a safety net.
|
||||||
|
pub fn close(self) {
|
||||||
|
// cleanup() runs here via Drop
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared cleanup — called by Drop (and thus by close).
|
||||||
|
fn cleanup(&mut self) {
|
||||||
|
self.unlock_tray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Debug aid — remove after fd issue is resolved
|
||||||
|
pub fn device_path_owned(&self) -> String {
|
||||||
|
self.device_path.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether this drive has a known profile (unlock parameters available).
|
/// Whether this drive has a known profile (unlock parameters available).
|
||||||
pub fn has_profile(&self) -> bool {
|
pub fn has_profile(&self) -> bool {
|
||||||
self.profile.is_some()
|
self.profile.is_some()
|
||||||
@@ -132,8 +155,8 @@ impl Drive {
|
|||||||
// Bits 1-0: door/tray state
|
// Bits 1-0: door/tray state
|
||||||
// Bit 1: media present, Bit 0: tray open
|
// Bit 1: media present, Bit 0: tray open
|
||||||
match media_status & 0x03 {
|
match media_status & 0x03 {
|
||||||
0x00 => DriveStatus::NoDisc, // tray closed, no disc
|
0x00 => DriveStatus::NoDisc, // tray closed, no disc
|
||||||
0x01 => DriveStatus::TrayOpen, // tray open
|
0x01 => DriveStatus::TrayOpen, // tray open
|
||||||
0x02 => DriveStatus::DiscPresent, // tray closed, disc present
|
0x02 => DriveStatus::DiscPresent, // tray closed, disc present
|
||||||
0x03 => DriveStatus::DiscPresent, // tray closed, disc present
|
0x03 => DriveStatus::DiscPresent, // tray closed, disc present
|
||||||
_ => DriveStatus::Unknown,
|
_ => DriveStatus::Unknown,
|
||||||
@@ -175,19 +198,24 @@ impl Drive {
|
|||||||
// 1. Unlock + stop/start
|
// 1. Unlock + stop/start
|
||||||
self.unlock_tray();
|
self.unlock_tray();
|
||||||
let stop = [0x1Bu8, 0x00, 0x00, 0x00, 0x00, 0x00];
|
let stop = [0x1Bu8, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||||
let _ = self.scsi.as_mut().execute(
|
let _ =
|
||||||
&stop, crate::scsi::DataDirection::None, &mut buf, 5_000,
|
self.scsi
|
||||||
);
|
.as_mut()
|
||||||
|
.execute(&stop, crate::scsi::DataDirection::None, &mut buf, 5_000);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
let start = [0x1Bu8, 0x00, 0x00, 0x00, 0x01, 0x00];
|
let start = [0x1Bu8, 0x00, 0x00, 0x00, 0x01, 0x00];
|
||||||
let _ = self.scsi.as_mut().execute(
|
let _ =
|
||||||
&start, crate::scsi::DataDirection::None, &mut buf, 5_000,
|
self.scsi
|
||||||
);
|
.as_mut()
|
||||||
|
.execute(&start, crate::scsi::DataDirection::None, &mut buf, 5_000);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||||
|
|
||||||
if self.scsi.as_mut().execute(
|
if self
|
||||||
&tur, crate::scsi::DataDirection::None, &mut buf, 5_000,
|
.scsi
|
||||||
).is_ok() {
|
.as_mut()
|
||||||
|
.execute(&tur, crate::scsi::DataDirection::None, &mut buf, 5_000)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,14 +224,17 @@ impl Drive {
|
|||||||
// counts as success: the drive is functional, just needs disc reinserted.
|
// counts as success: the drive is functional, just needs disc reinserted.
|
||||||
self.unlock_tray();
|
self.unlock_tray();
|
||||||
let eject = [0x1Bu8, 0x00, 0x00, 0x00, 0x02, 0x00];
|
let eject = [0x1Bu8, 0x00, 0x00, 0x00, 0x02, 0x00];
|
||||||
let _ = self.scsi.as_mut().execute(
|
let _ =
|
||||||
&eject, crate::scsi::DataDirection::None, &mut buf, 30_000,
|
self.scsi
|
||||||
);
|
.as_mut()
|
||||||
|
.execute(&eject, crate::scsi::DataDirection::None, &mut buf, 30_000);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||||
|
|
||||||
match self.scsi.as_mut().execute(
|
match self
|
||||||
&tur, crate::scsi::DataDirection::None, &mut buf, 5_000,
|
.scsi
|
||||||
) {
|
.as_mut()
|
||||||
|
.execute(&tur, crate::scsi::DataDirection::None, &mut buf, 5_000)
|
||||||
|
{
|
||||||
Ok(_) => return Ok(()),
|
Ok(_) => return Ok(()),
|
||||||
Err(Error::ScsiError { sense_key: 2, .. }) => return Ok(()), // tray open = valid
|
Err(Error::ScsiError { sense_key: 2, .. }) => return Ok(()), // tray open = valid
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -214,7 +245,10 @@ impl Drive {
|
|||||||
self.init()?;
|
self.init()?;
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||||
match self.scsi.as_mut().execute(
|
match self.scsi.as_mut().execute(
|
||||||
&tur, crate::scsi::DataDirection::None, &mut buf, 5_000,
|
&tur,
|
||||||
|
crate::scsi::DataDirection::None,
|
||||||
|
&mut buf,
|
||||||
|
5_000,
|
||||||
) {
|
) {
|
||||||
Ok(_) => return Ok(()),
|
Ok(_) => return Ok(()),
|
||||||
Err(Error::ScsiError { sense_key: 2, .. }) => return Ok(()),
|
Err(Error::ScsiError { sense_key: 2, .. }) => return Ok(()),
|
||||||
@@ -269,13 +303,28 @@ impl Drive {
|
|||||||
/// Returns the feature data (without the 8-byte header), or None if not available.
|
/// Returns the feature data (without the 8-byte header), or None if not available.
|
||||||
pub fn get_config_feature(&mut self, feature_code: u16) -> Option<Vec<u8>> {
|
pub fn get_config_feature(&mut self, feature_code: u16) -> Option<Vec<u8>> {
|
||||||
let cdb = [
|
let cdb = [
|
||||||
crate::scsi::SCSI_GET_CONFIGURATION, 0x02,
|
crate::scsi::SCSI_GET_CONFIGURATION,
|
||||||
(feature_code >> 8) as u8, feature_code as u8,
|
0x02,
|
||||||
0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
(feature_code >> 8) as u8,
|
||||||
|
feature_code as u8,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
];
|
];
|
||||||
let mut buf = vec![0u8; 256];
|
let mut buf = vec![0u8; 256];
|
||||||
let r = self.scsi.as_mut()
|
let r = self
|
||||||
.execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?;
|
.scsi
|
||||||
|
.as_mut()
|
||||||
|
.execute(
|
||||||
|
&cdb,
|
||||||
|
crate::scsi::DataDirection::FromDevice,
|
||||||
|
&mut buf,
|
||||||
|
5_000,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
if r.bytes_transferred > 8 {
|
if r.bytes_transferred > 8 {
|
||||||
Some(buf[8..r.bytes_transferred].to_vec())
|
Some(buf[8..r.bytes_transferred].to_vec())
|
||||||
} else {
|
} else {
|
||||||
@@ -285,29 +334,67 @@ impl Drive {
|
|||||||
|
|
||||||
/// Read REPORT KEY RPC state (region playback control).
|
/// Read REPORT KEY RPC state (region playback control).
|
||||||
pub fn report_key_rpc_state(&mut self) -> Option<Vec<u8>> {
|
pub fn report_key_rpc_state(&mut self) -> Option<Vec<u8>> {
|
||||||
let cdb = [0xA4u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00];
|
let cdb = [
|
||||||
|
0xA4u8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00,
|
||||||
|
];
|
||||||
let mut buf = vec![0u8; 8];
|
let mut buf = vec![0u8; 8];
|
||||||
let r = self.scsi.as_mut()
|
let r = self
|
||||||
.execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?;
|
.scsi
|
||||||
if r.bytes_transferred > 0 { Some(buf[..r.bytes_transferred].to_vec()) } else { None }
|
.as_mut()
|
||||||
|
.execute(
|
||||||
|
&cdb,
|
||||||
|
crate::scsi::DataDirection::FromDevice,
|
||||||
|
&mut buf,
|
||||||
|
5_000,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
if r.bytes_transferred > 0 {
|
||||||
|
Some(buf[..r.bytes_transferred].to_vec())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read MODE SENSE page data.
|
/// Read MODE SENSE page data.
|
||||||
pub fn mode_sense_page(&mut self, page: u8) -> Option<Vec<u8>> {
|
pub fn mode_sense_page(&mut self, page: u8) -> Option<Vec<u8>> {
|
||||||
let cdb = [0x5Au8, 0x00, page, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00];
|
let cdb = [0x5Au8, 0x00, page, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00];
|
||||||
let mut buf = vec![0u8; 252];
|
let mut buf = vec![0u8; 252];
|
||||||
let r = self.scsi.as_mut()
|
let r = self
|
||||||
.execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?;
|
.scsi
|
||||||
if r.bytes_transferred > 0 { Some(buf[..r.bytes_transferred].to_vec()) } else { None }
|
.as_mut()
|
||||||
|
.execute(
|
||||||
|
&cdb,
|
||||||
|
crate::scsi::DataDirection::FromDevice,
|
||||||
|
&mut buf,
|
||||||
|
5_000,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
if r.bytes_transferred > 0 {
|
||||||
|
Some(buf[..r.bytes_transferred].to_vec())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read vendor-specific READ BUFFER data.
|
/// Read vendor-specific READ BUFFER data.
|
||||||
pub fn read_buffer(&mut self, mode: u8, buffer_id: u8, length: u16) -> Option<Vec<u8>> {
|
pub fn read_buffer(&mut self, mode: u8, buffer_id: u8, length: u16) -> Option<Vec<u8>> {
|
||||||
let cdb = crate::scsi::build_read_buffer(mode, buffer_id, 0, length as u32);
|
let cdb = crate::scsi::build_read_buffer(mode, buffer_id, 0, length as u32);
|
||||||
let mut buf = vec![0u8; length as usize];
|
let mut buf = vec![0u8; length as usize];
|
||||||
let r = self.scsi.as_mut()
|
let r = self
|
||||||
.execute(&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000).ok()?;
|
.scsi
|
||||||
if r.bytes_transferred > 0 { Some(buf[..r.bytes_transferred].to_vec()) } else { None }
|
.as_mut()
|
||||||
|
.execute(
|
||||||
|
&cdb,
|
||||||
|
crate::scsi::DataDirection::FromDevice,
|
||||||
|
&mut buf,
|
||||||
|
5_000,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
if r.bytes_transferred > 0 {
|
||||||
|
Some(buf[..r.bytes_transferred].to_vec())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_ready(&self) -> bool {
|
pub fn is_ready(&self) -> bool {
|
||||||
@@ -317,8 +404,16 @@ impl Drive {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read sectors from the disc. Raw SCSI READ(10).
|
/// Read sectors from the disc with automatic error recovery.
|
||||||
|
///
|
||||||
|
/// On failure: drops to min speed, waits with escalating patience
|
||||||
|
/// (5s, 10s, 15s, 30s, 60s), resets drive between attempts.
|
||||||
|
/// After recovery, stays at min speed for 500 MB before ramping up.
|
||||||
|
///
|
||||||
|
/// Returns Err only after all attempts exhausted — user should clean
|
||||||
|
/// the disc and resume.
|
||||||
pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||||
|
let timeout_ms = if self.recovery_bytes_remaining > 0 { 30_000 } else { 10_000 };
|
||||||
let cdb = [
|
let cdb = [
|
||||||
crate::scsi::SCSI_READ_10,
|
crate::scsi::SCSI_READ_10,
|
||||||
0x00,
|
0x00,
|
||||||
@@ -331,24 +426,112 @@ impl Drive {
|
|||||||
count as u8,
|
count as u8,
|
||||||
0x00,
|
0x00,
|
||||||
];
|
];
|
||||||
let result = self.scsi.as_mut().execute(
|
|
||||||
&cdb,
|
// Normal read
|
||||||
crate::scsi::DataDirection::FromDevice,
|
match self.scsi.as_mut().execute(
|
||||||
buf,
|
&cdb, crate::scsi::DataDirection::FromDevice, buf, timeout_ms,
|
||||||
30_000,
|
) {
|
||||||
)?;
|
Ok(result) => {
|
||||||
Ok(result.bytes_transferred)
|
if self.recovery_bytes_remaining > 0 {
|
||||||
|
let bytes_read = count as u64 * 2048;
|
||||||
|
self.recovery_bytes_remaining =
|
||||||
|
self.recovery_bytes_remaining.saturating_sub(bytes_read);
|
||||||
|
if self.recovery_bytes_remaining == 0 {
|
||||||
|
eprintln!("[drive] recovery window complete — resuming full speed");
|
||||||
|
self.set_speed(0xFFFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Ok(result.bytes_transferred);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[drive] read error at LBA {} count {} — {}", lba, count, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1: gentle — sleep 30s, retry. 5 times.
|
||||||
|
// No intervention, just patience.
|
||||||
|
self.set_speed(0);
|
||||||
|
|
||||||
|
for attempt in 1..=5 {
|
||||||
|
eprintln!("[drive] phase 1 retry {}/5 at LBA {} — sleep 30s", attempt, lba);
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(30));
|
||||||
|
|
||||||
|
match self.scsi.as_mut().execute(
|
||||||
|
&cdb, crate::scsi::DataDirection::FromDevice, buf, 30_000,
|
||||||
|
) {
|
||||||
|
Ok(result) => {
|
||||||
|
eprintln!("[drive] phase 1 retry {}/5 OK at LBA {}", attempt, lba);
|
||||||
|
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
||||||
|
return Ok(result.bytes_transferred);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[drive] phase 1 retry {}/5 FAILED at LBA {} — {}", attempt, lba, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: fresh start — close, reset, open, init. Like restarting the app.
|
||||||
|
eprintln!("[drive] phase 2: fresh start at LBA {}", lba);
|
||||||
|
let device = std::path::PathBuf::from(&self.device_path);
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
|
let _ = crate::scsi::reset(&device);
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
|
self.scsi = match crate::scsi::open(&device) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[drive] reopen failed: {}", e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = self.init();
|
||||||
|
let _ = self.wait_ready();
|
||||||
|
self.set_speed(0);
|
||||||
|
|
||||||
|
// Phase 3: gentle again on fresh connection — sleep 30s, retry. 5 times.
|
||||||
|
for attempt in 1..=5 {
|
||||||
|
eprintln!("[drive] phase 3 retry {}/5 at LBA {} — sleep 30s", attempt, lba);
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(30));
|
||||||
|
|
||||||
|
match self.scsi.as_mut().execute(
|
||||||
|
&cdb, crate::scsi::DataDirection::FromDevice, buf, 30_000,
|
||||||
|
) {
|
||||||
|
Ok(result) => {
|
||||||
|
eprintln!("[drive] phase 3 retry {}/5 OK at LBA {}", attempt, lba);
|
||||||
|
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
||||||
|
return Ok(result.bytes_transferred);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[drive] phase 3 retry {}/5 FAILED at LBA {} — {}", attempt, lba, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both phases failed. Give up.
|
||||||
|
eprintln!("[drive] FAILED LBA {} count {} — all recovery exhausted", lba, count);
|
||||||
|
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
||||||
|
Err(Error::DiscRead { sector: lba as u64 })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the disc capacity in sectors (2048 bytes each).
|
/// Read the disc capacity in sectors (2048 bytes each).
|
||||||
pub fn read_capacity(&mut self) -> Result<u32> {
|
pub fn read_capacity(&mut self) -> Result<u32> {
|
||||||
let cdb = [
|
let cdb = [
|
||||||
crate::scsi::SCSI_READ_CAPACITY, 0x00, 0x00, 0x00, 0x00,
|
crate::scsi::SCSI_READ_CAPACITY,
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
];
|
];
|
||||||
let mut buf = [0u8; 8];
|
let mut buf = [0u8; 8];
|
||||||
self.scsi.as_mut().execute(
|
self.scsi.as_mut().execute(
|
||||||
&cdb, crate::scsi::DataDirection::FromDevice, &mut buf, 5_000,
|
&cdb,
|
||||||
|
crate::scsi::DataDirection::FromDevice,
|
||||||
|
&mut buf,
|
||||||
|
5_000,
|
||||||
)?;
|
)?;
|
||||||
let last_lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
|
let last_lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
|
||||||
Ok(last_lba + 1)
|
Ok(last_lba + 1)
|
||||||
@@ -364,18 +547,20 @@ impl Drive {
|
|||||||
pub fn lock_tray(&mut self) {
|
pub fn lock_tray(&mut self) {
|
||||||
let prevent = [0x1Eu8, 0x00, 0x00, 0x00, 0x01, 0x00];
|
let prevent = [0x1Eu8, 0x00, 0x00, 0x00, 0x01, 0x00];
|
||||||
let mut buf = [0u8; 0];
|
let mut buf = [0u8; 0];
|
||||||
let _ = self.scsi.as_mut().execute(
|
let _ =
|
||||||
&prevent, crate::scsi::DataDirection::None, &mut buf, 5_000,
|
self.scsi
|
||||||
);
|
.as_mut()
|
||||||
|
.execute(&prevent, crate::scsi::DataDirection::None, &mut buf, 5_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unlock the tray so the user can manually eject the disc.
|
/// Unlock the tray so the user can manually eject the disc.
|
||||||
pub fn unlock_tray(&mut self) {
|
pub fn unlock_tray(&mut self) {
|
||||||
let allow = [0x1Eu8, 0x00, 0x00, 0x00, 0x00, 0x00];
|
let allow = [0x1Eu8, 0x00, 0x00, 0x00, 0x00, 0x00];
|
||||||
let mut buf = [0u8; 0];
|
let mut buf = [0u8; 0];
|
||||||
let _ = self.scsi.as_mut().execute(
|
let _ =
|
||||||
&allow, crate::scsi::DataDirection::None, &mut buf, 5_000,
|
self.scsi
|
||||||
);
|
.as_mut()
|
||||||
|
.execute(&allow, crate::scsi::DataDirection::None, &mut buf, 5_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Eject the disc tray. Unlocks first, then ejects.
|
/// Eject the disc tray. Unlocks first, then ejects.
|
||||||
@@ -403,6 +588,13 @@ impl Drive {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for Drive {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.cleanup();
|
||||||
|
// SgIoTransport::drop() runs next, calling libc::close(fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SectorReader for Drive {
|
impl SectorReader for Drive {
|
||||||
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||||
self.read(lba, count, buf)
|
self.read(lba, count, buf)
|
||||||
@@ -441,6 +633,7 @@ fn discover_drives() -> Vec<(String, DriveId)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a device path to its raw SCSI device, with optional warning message.
|
/// Resolve a device path to its raw SCSI device, with optional warning message.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
|
pub(crate) fn resolve_device(path: &str) -> Result<(String, Option<String>)> {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
|
|||||||
+255
-371
@@ -1,273 +1,307 @@
|
|||||||
//! DiscStream — read BD-TS data from an optical disc drive.
|
//! DiscStream — read sectors from an optical disc drive.
|
||||||
//!
|
//!
|
||||||
//! Read-only stream. Wraps Drive + Disc.
|
//! `DiscStream::open()` does the full init sequence:
|
||||||
//! Handles drive init, AACS decryption, and sector reading.
|
//! drive open → wait_ready → init → probe_disc → scan
|
||||||
//!
|
//!
|
||||||
//! Reading state (extent index, offset, batch size, error recovery) is stored
|
//! Then reads title extents or full-disc sequentially.
|
||||||
//! directly on the struct so that successive `read()` calls advance through
|
//! No decryption — that's a caller concern.
|
||||||
//! the disc instead of restarting from byte 0.
|
|
||||||
|
|
||||||
use super::IOStream;
|
use super::IOStream;
|
||||||
use crate::disc::{
|
use crate::disc::{
|
||||||
detect_max_batch_sectors, ContentFormat, Disc, DiscTitle, Extent, MIN_BATCH_SECTORS,
|
detect_max_batch_sectors, Disc, DiscTitle, Extent, ScanOptions,
|
||||||
RAMP_BATCH_AFTER, RAMP_SPEED_AFTER, SLOW_SPEED_AFTER,
|
|
||||||
};
|
};
|
||||||
use crate::drive::Drive;
|
use crate::drive::Drive;
|
||||||
use crate::error::Error;
|
use crate::error::{Error, Result};
|
||||||
use crate::speed::DriveSpeed;
|
use crate::event::{Event, EventKind};
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
/// Options for opening a disc stream.
|
/// Optical disc stream. Read-only — yields raw sector bytes.
|
||||||
#[derive(Default)]
|
|
||||||
pub struct DiscOptions {
|
|
||||||
/// Device path (e.g. "/dev/sg4"). None = auto-detect.
|
|
||||||
pub device: Option<std::path::PathBuf>,
|
|
||||||
/// KEYDB.cfg path. None = search standard locations.
|
|
||||||
pub keydb_path: Option<std::path::PathBuf>,
|
|
||||||
/// Which title to read (0-based). None = longest title.
|
|
||||||
pub title_index: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optical disc stream. Read-only — yields decrypted BD-TS bytes.
|
|
||||||
///
|
///
|
||||||
/// Embeds the reading state that `ContentReader` would normally hold, so that
|
/// Created from an initialized Drive + title extents or full-disc mode.
|
||||||
/// successive `read()` calls advance through the disc correctly.
|
/// Error recovery (batch reduction, retry, zero-fill) is handled internally.
|
||||||
pub struct DiscStream {
|
pub struct DiscStream {
|
||||||
disc_title: DiscTitle,
|
drive: Drive,
|
||||||
disc: Disc,
|
title: DiscTitle,
|
||||||
session: Drive,
|
|
||||||
// Read buffer: holds one decoded batch
|
|
||||||
batch_buf: Vec<u8>,
|
|
||||||
batch_pos: usize,
|
|
||||||
eof: bool,
|
|
||||||
|
|
||||||
// ── Reading state (replaces ContentReader) ──
|
// What to read
|
||||||
extents: Vec<Extent>,
|
mode: ReadMode,
|
||||||
|
|
||||||
|
// Position
|
||||||
|
current_lba: u32,
|
||||||
current_extent: usize,
|
current_extent: usize,
|
||||||
current_offset: u32,
|
current_offset: u32,
|
||||||
#[allow(dead_code)]
|
|
||||||
content_format: ContentFormat,
|
// Buffer
|
||||||
decrypt_keys: crate::decrypt::DecryptKeys,
|
|
||||||
unit_key_idx: usize,
|
|
||||||
read_buf: Vec<u8>,
|
read_buf: Vec<u8>,
|
||||||
/// Current batch size in sectors (adapts on errors)
|
buf_valid: usize,
|
||||||
|
buf_cursor: usize,
|
||||||
|
|
||||||
|
// Batch size for reads
|
||||||
batch_sectors: u16,
|
batch_sectors: u16,
|
||||||
/// Maximum batch size detected from kernel limits
|
pub errors: u64,
|
||||||
max_batch_sectors: u16,
|
eof: bool,
|
||||||
/// Consecutive successful batch reads
|
}
|
||||||
ok_streak: u32,
|
|
||||||
/// Consecutive errors at current position
|
enum ReadMode {
|
||||||
error_streak: u32,
|
/// Read title extents (for MKV, M2TS, etc.)
|
||||||
/// Total read errors encountered
|
Extents(Vec<Extent>),
|
||||||
pub errors: u32,
|
/// Read LBA 0 to capacity (for ISO)
|
||||||
|
Sequential { capacity: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of opening a DiscStream.
|
||||||
|
pub struct DiscOpenResult {
|
||||||
|
pub stream: DiscStream,
|
||||||
|
pub disc: Disc,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiscStream {
|
impl DiscStream {
|
||||||
/// Open the disc drive and scan disc metadata.
|
/// Open a disc drive, init, scan, and prepare to read a title.
|
||||||
pub fn open(opts: DiscOptions) -> Result<Self, Error> {
|
///
|
||||||
let mut session = match opts.device {
|
/// Steps (each does one thing):
|
||||||
Some(ref d) => Drive::open(d)?,
|
/// 1. Drive::open (or find_drive)
|
||||||
|
/// 2. wait_ready
|
||||||
|
/// 3. init (non-fatal)
|
||||||
|
/// 4. probe_disc (non-fatal)
|
||||||
|
/// 5. Disc::scan
|
||||||
|
///
|
||||||
|
/// Pass an event callback for status reporting, or None.
|
||||||
|
pub fn open(
|
||||||
|
device: Option<&Path>,
|
||||||
|
keydb_path: Option<&str>,
|
||||||
|
title_index: usize,
|
||||||
|
on_event: Option<&dyn Fn(Event)>,
|
||||||
|
) -> Result<DiscOpenResult> {
|
||||||
|
let emit = |kind: EventKind| {
|
||||||
|
if let Some(cb) = &on_event {
|
||||||
|
cb(Event { kind });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Open
|
||||||
|
let mut drive = match device {
|
||||||
|
Some(d) => Drive::open(d)?,
|
||||||
None => crate::drive::find_drive().ok_or_else(|| Error::DeviceNotFound {
|
None => crate::drive::find_drive().ok_or_else(|| Error::DeviceNotFound {
|
||||||
path: String::new(),
|
path: String::new(),
|
||||||
})?,
|
})?,
|
||||||
};
|
};
|
||||||
session.wait_ready()?;
|
emit(EventKind::DriveOpened {
|
||||||
let _ = session.init();
|
device: drive.device_path().to_string(),
|
||||||
let _ = session.probe_disc();
|
});
|
||||||
|
|
||||||
let scan_opts = match opts.keydb_path {
|
// 2. Wait
|
||||||
Some(ref kp) => crate::disc::ScanOptions::with_keydb(kp.clone()),
|
let _ = drive.wait_ready();
|
||||||
None => crate::disc::ScanOptions::default(),
|
emit(EventKind::DriveReady);
|
||||||
|
|
||||||
|
// 3. Init
|
||||||
|
let init_ok = drive.init().is_ok();
|
||||||
|
emit(EventKind::InitComplete { success: init_ok });
|
||||||
|
|
||||||
|
// 4. Probe
|
||||||
|
let probe_ok = drive.probe_disc().is_ok();
|
||||||
|
emit(EventKind::ProbeComplete { success: probe_ok });
|
||||||
|
|
||||||
|
// 5. Scan
|
||||||
|
let scan_opts = match keydb_path {
|
||||||
|
Some(kp) => ScanOptions::with_keydb(kp),
|
||||||
|
None => ScanOptions::default(),
|
||||||
};
|
};
|
||||||
let disc = Disc::scan(&mut session, &scan_opts)?;
|
let disc = Disc::scan(&mut drive, &scan_opts)?;
|
||||||
|
emit(EventKind::ScanComplete {
|
||||||
|
titles: disc.titles.len(),
|
||||||
|
});
|
||||||
|
|
||||||
let title_index = opts.title_index.unwrap_or(0);
|
|
||||||
if title_index >= disc.titles.len() {
|
if title_index >= disc.titles.len() {
|
||||||
return Err(Error::DiscTitleRange {
|
return Err(Error::DiscTitleRange {
|
||||||
index: title_index,
|
index: title_index,
|
||||||
count: disc.titles.len(),
|
count: disc.titles.len(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let disc_title = disc.titles[title_index].clone();
|
|
||||||
let extents = disc_title.extents.clone();
|
|
||||||
let content_format = disc_title.content_format;
|
|
||||||
let decrypt_keys = disc.decrypt_keys();
|
|
||||||
|
|
||||||
let max_batch = detect_max_batch_sectors(session.device_path());
|
let title = disc.titles[title_index].clone();
|
||||||
|
let stream = Self::title(drive, title);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(DiscOpenResult { stream, disc })
|
||||||
disc_title,
|
}
|
||||||
disc,
|
|
||||||
session,
|
/// Create a stream that reads a title's extents.
|
||||||
batch_buf: Vec::new(),
|
/// Use this when you already have an initialized Drive.
|
||||||
batch_pos: 0,
|
pub fn title(drive: Drive, title: DiscTitle) -> Self {
|
||||||
eof: false,
|
let max_batch = detect_max_batch_sectors(drive.device_path());
|
||||||
extents,
|
let extents = title.extents.clone();
|
||||||
|
Self::new(drive, title, ReadMode::Extents(extents), max_batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a stream that reads the full disc sequentially (for ISO).
|
||||||
|
pub fn full_disc(drive: Drive, title: DiscTitle, capacity: u32) -> Self {
|
||||||
|
let max_batch = detect_max_batch_sectors(drive.device_path());
|
||||||
|
Self::new(drive, title, ReadMode::Sequential { capacity }, max_batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resume a full disc read from a given LBA (for ISO resume).
|
||||||
|
/// Use after checking an existing partial file:
|
||||||
|
/// start_lba = (file_size / 2048) - safety_margin
|
||||||
|
pub fn full_disc_resume(drive: Drive, title: DiscTitle, capacity: u32, start_lba: u32) -> Self {
|
||||||
|
let max_batch = detect_max_batch_sectors(drive.device_path());
|
||||||
|
let mut stream = Self::new(drive, title, ReadMode::Sequential { capacity }, max_batch);
|
||||||
|
stream.current_lba = start_lba;
|
||||||
|
stream
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set SCSI read timeout (default 30s).
|
||||||
|
|
||||||
|
fn new(drive: Drive, title: DiscTitle, mode: ReadMode, max_batch: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
drive,
|
||||||
|
title,
|
||||||
|
mode,
|
||||||
|
current_lba: 0,
|
||||||
current_extent: 0,
|
current_extent: 0,
|
||||||
current_offset: 0,
|
current_offset: 0,
|
||||||
content_format,
|
|
||||||
decrypt_keys,
|
|
||||||
unit_key_idx: 0,
|
|
||||||
read_buf: Vec::with_capacity(max_batch as usize * 2048),
|
read_buf: Vec::with_capacity(max_batch as usize * 2048),
|
||||||
|
buf_valid: 0,
|
||||||
|
buf_cursor: 0,
|
||||||
batch_sectors: max_batch,
|
batch_sectors: max_batch,
|
||||||
max_batch_sectors: max_batch,
|
|
||||||
ok_streak: 0,
|
|
||||||
error_streak: 0,
|
|
||||||
errors: 0,
|
errors: 0,
|
||||||
})
|
eof: false,
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the full Disc (for listing all titles, etc.)
|
|
||||||
pub fn disc(&self) -> &Disc {
|
|
||||||
&self.disc
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read sectors from the drive into `self.read_buf`.
|
|
||||||
fn read_sectors(&mut self, lba: u32, count: u16) -> Result<(), Error> {
|
|
||||||
self.session.read(lba, count, &mut self.read_buf)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fill the internal read buffer with the next batch of sectors,
|
|
||||||
/// handling error recovery (halve batch, slow drive, retry, skip).
|
|
||||||
///
|
|
||||||
/// Returns `true` if data was read, `false` at end-of-title.
|
|
||||||
fn fill_buffer(&mut self) -> Result<bool, Error> {
|
|
||||||
loop {
|
|
||||||
if self.current_extent >= self.extents.len() {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ext_start = self.extents[self.current_extent].start_lba;
|
|
||||||
let ext_sectors = self.extents[self.current_extent].sector_count;
|
|
||||||
let remaining = ext_sectors.saturating_sub(self.current_offset);
|
|
||||||
|
|
||||||
// Align to 3 sectors (one aligned unit)
|
|
||||||
let sectors_to_read = remaining.min(self.batch_sectors as u32) as u16;
|
|
||||||
let sectors_to_read = sectors_to_read - (sectors_to_read % 3);
|
|
||||||
if sectors_to_read == 0 {
|
|
||||||
self.current_extent += 1;
|
|
||||||
self.current_offset = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lba = ext_start + self.current_offset;
|
|
||||||
let byte_count = sectors_to_read as usize * 2048;
|
|
||||||
self.read_buf.resize(byte_count, 0);
|
|
||||||
|
|
||||||
match self.read_sectors(lba, sectors_to_read) {
|
|
||||||
Ok(_) => {
|
|
||||||
self.current_offset += sectors_to_read as u32;
|
|
||||||
self.error_streak = 0;
|
|
||||||
|
|
||||||
if self.current_offset >= ext_sectors {
|
|
||||||
self.current_extent += 1;
|
|
||||||
self.current_offset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ramp up batch size after consecutive successes
|
|
||||||
self.ok_streak += 1;
|
|
||||||
if self.batch_sectors < self.max_batch_sectors
|
|
||||||
&& self.ok_streak >= RAMP_BATCH_AFTER
|
|
||||||
{
|
|
||||||
self.batch_sectors = (self.batch_sectors * 2).min(self.max_batch_sectors);
|
|
||||||
self.ok_streak = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore max speed after sustained success at full batch
|
|
||||||
if self.batch_sectors == self.max_batch_sectors
|
|
||||||
&& self.ok_streak >= RAMP_SPEED_AFTER
|
|
||||||
{
|
|
||||||
self.session.set_speed(0xFFFF);
|
|
||||||
self.ok_streak = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
self.errors += 1;
|
|
||||||
self.error_streak += 1;
|
|
||||||
self.ok_streak = 0;
|
|
||||||
|
|
||||||
// First error: re-init (drive may have re-locked)
|
|
||||||
if self.error_streak == 1 {
|
|
||||||
let _ = self.session.init();
|
|
||||||
let _ = self.session.probe_disc();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repeated errors: slow down
|
|
||||||
if self.error_streak >= SLOW_SPEED_AFTER {
|
|
||||||
self.session.set_speed(DriveSpeed::BD2x.to_kbps());
|
|
||||||
self.error_streak = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.batch_sectors > MIN_BATCH_SECTORS {
|
|
||||||
self.batch_sectors = (self.batch_sectors / 2).max(MIN_BATCH_SECTORS);
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
||||||
} else {
|
|
||||||
// At minimum batch -- retry once with longer pause
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
||||||
self.read_buf.resize(MIN_BATCH_SECTORS as usize * 2048, 0);
|
|
||||||
if self.read_sectors(lba, MIN_BATCH_SECTORS).is_ok() {
|
|
||||||
self.error_streak = 0;
|
|
||||||
self.current_offset += MIN_BATCH_SECTORS as u32;
|
|
||||||
if self.current_offset >= ext_sectors {
|
|
||||||
self.current_extent += 1;
|
|
||||||
self.current_offset = 0;
|
|
||||||
}
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
// Still failing -- skip this unit (zero-fill)
|
|
||||||
self.current_offset += 3;
|
|
||||||
if self.current_offset >= ext_sectors {
|
|
||||||
self.current_extent += 1;
|
|
||||||
self.current_offset = 0;
|
|
||||||
}
|
|
||||||
self.read_buf.resize(crate::aacs::ALIGNED_UNIT_LEN, 0);
|
|
||||||
self.read_buf.fill(0);
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt the contents of `self.read_buf` in-place and copy the
|
/// Lock the tray.
|
||||||
/// decrypted data into `self.batch_buf`.
|
pub fn lock_tray(&mut self) {
|
||||||
fn decrypt_and_buffer(&mut self) {
|
self.drive.lock_tray();
|
||||||
let total_bytes = self.read_buf.len();
|
|
||||||
crate::decrypt::decrypt_sectors(
|
|
||||||
&mut self.read_buf[..total_bytes],
|
|
||||||
&self.decrypt_keys,
|
|
||||||
self.unit_key_idx,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Swap buffers instead of copying — the old batch_buf becomes
|
|
||||||
// read_buf and will be overwritten on the next read.
|
|
||||||
std::mem::swap(&mut self.batch_buf, &mut self.read_buf);
|
|
||||||
self.batch_pos = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unlock the tray.
|
||||||
|
pub fn unlock_tray(&mut self) {
|
||||||
|
self.drive.unlock_tray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recover the drive (for batch: switch to another title).
|
||||||
|
pub fn into_drive(self) -> Drive {
|
||||||
|
self.drive
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fill ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn fill(&mut self) -> bool {
|
||||||
|
match &self.mode {
|
||||||
|
ReadMode::Extents(_) => self.fill_extents(),
|
||||||
|
ReadMode::Sequential { .. } => self.fill_sequential(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_extents(&mut self) -> bool {
|
||||||
|
let (ext_start, ext_sectors) = match &self.mode {
|
||||||
|
ReadMode::Extents(exts) => {
|
||||||
|
if self.current_extent >= exts.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
(
|
||||||
|
exts[self.current_extent].start_lba,
|
||||||
|
exts[self.current_extent].sector_count,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let remaining = ext_sectors.saturating_sub(self.current_offset);
|
||||||
|
let sectors = remaining.min(self.batch_sectors as u32) as u16;
|
||||||
|
let sectors = sectors - (sectors % 3);
|
||||||
|
if sectors == 0 {
|
||||||
|
self.current_extent += 1;
|
||||||
|
self.current_offset = 0;
|
||||||
|
return self.fill_extents(); // next extent
|
||||||
|
}
|
||||||
|
|
||||||
|
let lba = ext_start + self.current_offset;
|
||||||
|
let bytes = sectors as usize * 2048;
|
||||||
|
self.read_buf.resize(bytes, 0);
|
||||||
|
|
||||||
|
// Drive handles all error recovery internally.
|
||||||
|
match self.drive.read(
|
||||||
|
lba,
|
||||||
|
sectors,
|
||||||
|
&mut self.read_buf[..bytes],
|
||||||
|
) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.buf_valid = bytes;
|
||||||
|
self.buf_cursor = 0;
|
||||||
|
self.current_offset += sectors as u32;
|
||||||
|
if self.current_offset >= ext_sectors {
|
||||||
|
self.current_extent += 1;
|
||||||
|
self.current_offset = 0;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(_) => false, // drive gone — EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_sequential(&mut self) -> bool {
|
||||||
|
let capacity = match &self.mode {
|
||||||
|
ReadMode::Sequential { capacity } => *capacity,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.current_lba >= capacity {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining = capacity - self.current_lba;
|
||||||
|
let count = remaining.min(self.batch_sectors as u32) as u16;
|
||||||
|
let bytes = count as usize * 2048;
|
||||||
|
self.read_buf.resize(bytes, 0);
|
||||||
|
|
||||||
|
// Drive handles all error recovery internally —
|
||||||
|
// retries, speed changes, zero-fill on unreadable sectors.
|
||||||
|
match self.drive.read(
|
||||||
|
self.current_lba,
|
||||||
|
count,
|
||||||
|
&mut self.read_buf[..bytes],
|
||||||
|
) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.buf_valid = bytes;
|
||||||
|
self.buf_cursor = 0;
|
||||||
|
self.current_lba += count as u32;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(_) => false, // drive gone — EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── IOStream ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
impl IOStream for DiscStream {
|
impl IOStream for DiscStream {
|
||||||
fn info(&self) -> &DiscTitle {
|
fn info(&self) -> &DiscTitle {
|
||||||
&self.disc_title
|
&self.title
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&mut self) -> io::Result<()> {
|
fn finish(&mut self) -> io::Result<()> {
|
||||||
|
self.drive.unlock_tray();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn total_bytes(&self) -> Option<u64> {
|
fn total_bytes(&self) -> Option<u64> {
|
||||||
Some(self.disc_title.size_bytes)
|
match &self.mode {
|
||||||
|
ReadMode::Extents(extents) => {
|
||||||
|
Some(extents.iter().map(|e| e.sector_count as u64 * 2048).sum())
|
||||||
|
}
|
||||||
|
ReadMode::Sequential { capacity } => Some(*capacity as u64 * 2048),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Read for DiscStream {
|
impl Read for DiscStream {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
// Drain buffer first
|
// Drain current buffer
|
||||||
if self.batch_pos < self.batch_buf.len() {
|
if self.buf_cursor < self.buf_valid {
|
||||||
let n = (self.batch_buf.len() - self.batch_pos).min(buf.len());
|
let n = (self.buf_valid - self.buf_cursor).min(buf.len());
|
||||||
buf[..n].copy_from_slice(&self.batch_buf[self.batch_pos..self.batch_pos + n]);
|
buf[..n].copy_from_slice(&self.read_buf[self.buf_cursor..self.buf_cursor + n]);
|
||||||
self.batch_pos += n;
|
self.buf_cursor += n;
|
||||||
return Ok(n);
|
return Ok(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,24 +309,16 @@ impl Read for DiscStream {
|
|||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill the read buffer with the next batch of sectors
|
// Fill next batch
|
||||||
let has_data = self
|
if self.fill() {
|
||||||
.fill_buffer()
|
let n = self.buf_valid.min(buf.len());
|
||||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
buf[..n].copy_from_slice(&self.read_buf[..n]);
|
||||||
|
self.buf_cursor = n;
|
||||||
if !has_data {
|
Ok(n)
|
||||||
|
} else {
|
||||||
self.eof = true;
|
self.eof = true;
|
||||||
return Ok(0);
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt in-place and move to batch_buf
|
|
||||||
self.decrypt_and_buffer();
|
|
||||||
|
|
||||||
// Now drain into the caller's buffer
|
|
||||||
let n = self.batch_buf.len().min(buf.len());
|
|
||||||
buf[..n].copy_from_slice(&self.batch_buf[..n]);
|
|
||||||
self.batch_pos = n;
|
|
||||||
Ok(n)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,145 +333,3 @@ impl Write for DiscStream {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::disc::Extent;
|
|
||||||
|
|
||||||
/// Build a minimal DiscStream with fake extents for testing state advancement.
|
|
||||||
/// We cannot call `DiscStream::open()` without a real drive, so we construct
|
|
||||||
/// one manually and then call the internal `fill_buffer` / `read` path
|
|
||||||
/// through a helper that simulates the session reads.
|
|
||||||
///
|
|
||||||
/// Instead we test the state-machine logic directly: given a set of extents
|
|
||||||
/// and a current_extent/current_offset, verify that repeated reads advance
|
|
||||||
/// through the extents correctly.
|
|
||||||
#[test]
|
|
||||||
fn state_advances_across_extents() {
|
|
||||||
// Simulate two extents of 6 sectors each (2 aligned units each).
|
|
||||||
let extents = [
|
|
||||||
Extent {
|
|
||||||
start_lba: 100,
|
|
||||||
sector_count: 6,
|
|
||||||
},
|
|
||||||
Extent {
|
|
||||||
start_lba: 200,
|
|
||||||
sector_count: 6,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Walk through the extents manually using the same arithmetic
|
|
||||||
// that fill_buffer uses, and verify we visit every sector.
|
|
||||||
let batch_sectors: u16 = 6;
|
|
||||||
let mut current_extent: usize = 0;
|
|
||||||
let mut current_offset: u32 = 0;
|
|
||||||
let mut lbas_read = Vec::new();
|
|
||||||
|
|
||||||
while current_extent < extents.len() {
|
|
||||||
let ext_start = extents[current_extent].start_lba;
|
|
||||||
let ext_sectors = extents[current_extent].sector_count;
|
|
||||||
let remaining = ext_sectors.saturating_sub(current_offset);
|
|
||||||
let sectors_to_read = remaining.min(batch_sectors as u32) as u16;
|
|
||||||
let sectors_to_read = sectors_to_read - (sectors_to_read % 3);
|
|
||||||
if sectors_to_read == 0 {
|
|
||||||
current_extent += 1;
|
|
||||||
current_offset = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let lba = ext_start + current_offset;
|
|
||||||
lbas_read.push((lba, sectors_to_read));
|
|
||||||
current_offset += sectors_to_read as u32;
|
|
||||||
if current_offset >= ext_sectors {
|
|
||||||
current_extent += 1;
|
|
||||||
current_offset = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(lbas_read.len(), 2, "should read two batches");
|
|
||||||
assert_eq!(lbas_read[0], (100, 6), "first batch starts at LBA 100");
|
|
||||||
assert_eq!(lbas_read[1], (200, 6), "second batch starts at LBA 200");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify that small extents that are not aligned to 3 sectors are skipped
|
|
||||||
/// (moved past) rather than causing an infinite loop.
|
|
||||||
#[test]
|
|
||||||
fn unaligned_extent_is_skipped() {
|
|
||||||
let extents = [
|
|
||||||
Extent {
|
|
||||||
start_lba: 50,
|
|
||||||
sector_count: 2, // < 3, cannot form an aligned unit
|
|
||||||
},
|
|
||||||
Extent {
|
|
||||||
start_lba: 300,
|
|
||||||
sector_count: 9,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let batch_sectors: u16 = 9;
|
|
||||||
let mut current_extent: usize = 0;
|
|
||||||
let mut current_offset: u32 = 0;
|
|
||||||
let mut lbas_read = Vec::new();
|
|
||||||
|
|
||||||
while current_extent < extents.len() {
|
|
||||||
let ext_start = extents[current_extent].start_lba;
|
|
||||||
let ext_sectors = extents[current_extent].sector_count;
|
|
||||||
let remaining = ext_sectors.saturating_sub(current_offset);
|
|
||||||
let sectors_to_read = remaining.min(batch_sectors as u32) as u16;
|
|
||||||
let sectors_to_read = sectors_to_read - (sectors_to_read % 3);
|
|
||||||
if sectors_to_read == 0 {
|
|
||||||
current_extent += 1;
|
|
||||||
current_offset = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let lba = ext_start + current_offset;
|
|
||||||
lbas_read.push((lba, sectors_to_read));
|
|
||||||
current_offset += sectors_to_read as u32;
|
|
||||||
if current_offset >= ext_sectors {
|
|
||||||
current_extent += 1;
|
|
||||||
current_offset = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(lbas_read.len(), 1, "only second extent is readable");
|
|
||||||
assert_eq!(lbas_read[0], (300, 9));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify that multiple reads from the same extent produce advancing offsets.
|
|
||||||
#[test]
|
|
||||||
fn multiple_batches_within_one_extent() {
|
|
||||||
let extents = [Extent {
|
|
||||||
start_lba: 1000,
|
|
||||||
sector_count: 18, // 6 aligned units = 3 batches of 6 sectors
|
|
||||||
}];
|
|
||||||
|
|
||||||
let batch_sectors: u16 = 6;
|
|
||||||
let mut current_extent: usize = 0;
|
|
||||||
let mut current_offset: u32 = 0;
|
|
||||||
let mut lbas_read = Vec::new();
|
|
||||||
|
|
||||||
while current_extent < extents.len() {
|
|
||||||
let ext_start = extents[current_extent].start_lba;
|
|
||||||
let ext_sectors = extents[current_extent].sector_count;
|
|
||||||
let remaining = ext_sectors.saturating_sub(current_offset);
|
|
||||||
let sectors_to_read = remaining.min(batch_sectors as u32) as u16;
|
|
||||||
let sectors_to_read = sectors_to_read - (sectors_to_read % 3);
|
|
||||||
if sectors_to_read == 0 {
|
|
||||||
current_extent += 1;
|
|
||||||
current_offset = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let lba = ext_start + current_offset;
|
|
||||||
lbas_read.push((lba, sectors_to_read));
|
|
||||||
current_offset += sectors_to_read as u32;
|
|
||||||
if current_offset >= ext_sectors {
|
|
||||||
current_extent += 1;
|
|
||||||
current_offset = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(lbas_read.len(), 3, "three batches from one extent");
|
|
||||||
assert_eq!(lbas_read[0], (1000, 6));
|
|
||||||
assert_eq!(lbas_read[1], (1006, 6));
|
|
||||||
assert_eq!(lbas_read[2], (1012, 6));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+25
-8
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
use super::isowriter::IsoWriter;
|
use super::isowriter::IsoWriter;
|
||||||
use super::IOStream;
|
use super::IOStream;
|
||||||
|
use crate::decrypt::DecryptKeys;
|
||||||
use crate::disc::{Disc, DiscTitle, ScanOptions};
|
use crate::disc::{Disc, DiscTitle, ScanOptions};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::sector::SectorReader;
|
use crate::sector::SectorReader;
|
||||||
@@ -71,6 +72,8 @@ pub struct IsoStream {
|
|||||||
buf_pos: usize,
|
buf_pos: usize,
|
||||||
buf_len: usize,
|
buf_len: usize,
|
||||||
eof: bool,
|
eof: bool,
|
||||||
|
/// Decrypt on read — auto-detected from disc scan.
|
||||||
|
decrypt_keys: DecryptKeys,
|
||||||
// Write side
|
// Write side
|
||||||
iso_writer: Option<IsoWriter<io::BufWriter<File>>>,
|
iso_writer: Option<IsoWriter<io::BufWriter<File>>>,
|
||||||
write_started: bool,
|
write_started: bool,
|
||||||
@@ -85,17 +88,26 @@ impl IsoStream {
|
|||||||
let disc = Disc::scan_image(&mut reader, capacity, opts)
|
let disc = Disc::scan_image(&mut reader, capacity, opts)
|
||||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
.map_err(|e| io::Error::other(e.to_string()))?;
|
||||||
|
|
||||||
let idx = title_index
|
if disc.titles.is_empty() {
|
||||||
.unwrap_or(0)
|
|
||||||
.min(disc.titles.len().saturating_sub(1));
|
|
||||||
let disc_title = if disc.titles.is_empty() {
|
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::NotFound,
|
io::ErrorKind::NotFound,
|
||||||
"no titles found in ISO image",
|
"no titles found in ISO image",
|
||||||
));
|
));
|
||||||
} else {
|
}
|
||||||
disc.titles[idx].clone()
|
let idx = title_index.unwrap_or(0);
|
||||||
};
|
if idx >= disc.titles.len() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!(
|
||||||
|
"title {} out of range (disc has {})",
|
||||||
|
idx + 1,
|
||||||
|
disc.titles.len()
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let disc_title = disc.titles[idx].clone();
|
||||||
|
|
||||||
|
let decrypt_keys = disc.decrypt_keys();
|
||||||
|
|
||||||
let extents: Vec<(u32, u32)> = disc_title
|
let extents: Vec<(u32, u32)> = disc_title
|
||||||
.extents
|
.extents
|
||||||
@@ -115,6 +127,7 @@ impl IsoStream {
|
|||||||
buf_pos: 0,
|
buf_pos: 0,
|
||||||
buf_len: 0,
|
buf_len: 0,
|
||||||
eof: false,
|
eof: false,
|
||||||
|
decrypt_keys,
|
||||||
iso_writer: None,
|
iso_writer: None,
|
||||||
write_started: false,
|
write_started: false,
|
||||||
})
|
})
|
||||||
@@ -130,6 +143,7 @@ impl IsoStream {
|
|||||||
Ok(IsoStream {
|
Ok(IsoStream {
|
||||||
disc_title: DiscTitle::empty(),
|
disc_title: DiscTitle::empty(),
|
||||||
disc: None,
|
disc: None,
|
||||||
|
decrypt_keys: DecryptKeys::None,
|
||||||
reader: None,
|
reader: None,
|
||||||
extents: Vec::new(),
|
extents: Vec::new(),
|
||||||
extent_idx: 0,
|
extent_idx: 0,
|
||||||
@@ -188,8 +202,11 @@ impl IsoStream {
|
|||||||
reader
|
reader
|
||||||
.read_sectors(lba, count, &mut self.batch_buf)
|
.read_sectors(lba, count, &mut self.batch_buf)
|
||||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
.map_err(|e| io::Error::other(e.to_string()))?;
|
||||||
|
|
||||||
|
let bytes = count as usize * SECTOR_SIZE as usize;
|
||||||
|
|
||||||
self.buf_pos = 0;
|
self.buf_pos = 0;
|
||||||
self.buf_len = count as usize * SECTOR_SIZE as usize;
|
self.buf_len = bytes;
|
||||||
|
|
||||||
self.sectors_remaining -= count as u32;
|
self.sectors_remaining -= count as u32;
|
||||||
if self.sectors_remaining == 0 {
|
if self.sectors_remaining == 0 {
|
||||||
|
|||||||
+187
-22
@@ -5,9 +5,13 @@ use crate::error::{Error, Result};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
const SG_IO: u32 = 0x2285;
|
const SG_IO: u32 = 0x2285;
|
||||||
|
const SG_SCSI_RESET: u32 = 0x2284;
|
||||||
|
const SG_SCSI_RESET_DEVICE: i32 = 1;
|
||||||
const SG_DXFER_NONE: i32 = -1;
|
const SG_DXFER_NONE: i32 = -1;
|
||||||
const SG_DXFER_TO_DEV: i32 = -2;
|
const SG_DXFER_TO_DEV: i32 = -2;
|
||||||
const SG_DXFER_FROM_DEV: i32 = -3;
|
const SG_DXFER_FROM_DEV: i32 = -3;
|
||||||
|
const SG_FLAG_DIRECT_IO: u32 = 1;
|
||||||
|
const SG_FLAG_Q_AT_HEAD: u32 = 0x10;
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
@@ -41,13 +45,12 @@ pub struct SgIoTransport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SgIoTransport {
|
impl SgIoTransport {
|
||||||
|
/// Open a SCSI device for use. Resets the drive first to ensure
|
||||||
|
/// a known good state, then opens a fresh fd for commands.
|
||||||
pub fn open(device: &Path) -> Result<Self> {
|
pub fn open(device: &Path) -> Result<Self> {
|
||||||
use std::os::unix::ffi::OsStrExt;
|
let device = Self::resolve_to_sg(device);
|
||||||
let path_bytes = device.as_os_str().as_bytes();
|
Self::reset(&device)?;
|
||||||
let mut c_path = Vec::with_capacity(path_bytes.len() + 1);
|
let c_path = Self::to_c_path(&device);
|
||||||
c_path.extend_from_slice(path_bytes);
|
|
||||||
c_path.push(0);
|
|
||||||
|
|
||||||
let fd = unsafe {
|
let fd = unsafe {
|
||||||
libc::open(
|
libc::open(
|
||||||
c_path.as_ptr() as *const libc::c_char,
|
c_path.as_ptr() as *const libc::c_char,
|
||||||
@@ -55,29 +58,183 @@ impl SgIoTransport {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
if fd < 0 {
|
if fd < 0 {
|
||||||
let err = std::io::Error::last_os_error();
|
return Self::open_error(&device);
|
||||||
return Err(if err.kind() == std::io::ErrorKind::PermissionDenied {
|
|
||||||
Error::DevicePermission {
|
|
||||||
path: format!(
|
|
||||||
"{}: permission denied (try running as root)",
|
|
||||||
device.display()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Error::DeviceNotFound {
|
|
||||||
path: device.display().to_string(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Ok(SgIoTransport { fd })
|
Ok(SgIoTransport { fd })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset the drive to a known good state — equivalent to unplug/replug.
|
||||||
|
/// After reset, the drive is clean and no fd is held open.
|
||||||
|
///
|
||||||
|
/// ## Why each step exists
|
||||||
|
///
|
||||||
|
/// When a process is killed (SIGKILL/kill -9) mid-SG_IO ioctl, two things
|
||||||
|
/// go wrong: (1) the kernel's SG driver may have stale pending commands
|
||||||
|
/// queued for the dead process's fd, and (2) the drive firmware may still
|
||||||
|
/// be mid-operation (seeking, reading, processing a vendor command).
|
||||||
|
///
|
||||||
|
/// A new process opening the same /dev/sg* device gets a fresh fd, but the
|
||||||
|
/// kernel doesn't automatically abort the dead process's commands — the
|
||||||
|
/// drive can appear hung on the first SCSI command.
|
||||||
|
///
|
||||||
|
/// Additionally, killed processes skip Drop, so the tray may be locked
|
||||||
|
/// via PREVENT MEDIUM REMOVAL with no process alive to unlock it.
|
||||||
|
///
|
||||||
|
/// ## Sequence
|
||||||
|
///
|
||||||
|
/// 1. **open** — allocates kernel SG state for this fd
|
||||||
|
/// 2. **close** — triggers kernel cleanup: aborts any pending SG_IO
|
||||||
|
/// commands associated with this fd. The key operation —
|
||||||
|
/// the kernel's sg_release() cancels queued commands.
|
||||||
|
/// 3. **sleep 2s** — the drive firmware needs time to finish/abort whatever
|
||||||
|
/// it was doing when the previous process died. Without
|
||||||
|
/// this, the next command may block on drive-internal state.
|
||||||
|
/// 4. **open** — fresh fd with no stale commands in the kernel queue
|
||||||
|
/// 5. **unlock** — ALLOW MEDIUM REMOVAL (CDB 0x1E, prevent=0). Clears
|
||||||
|
/// any tray lock left by a killed process that never
|
||||||
|
/// ran its Drop/cleanup.
|
||||||
|
/// 6. **TUR** — TEST UNIT READY (CDB 0x00) with 3s timeout. If the
|
||||||
|
/// drive responds, it's in a good state.
|
||||||
|
/// 7. **escalate** — if TUR fails:
|
||||||
|
/// a. SG_SCSI_RESET (device level) — kernel sends a SCSI
|
||||||
|
/// bus reset to the device, clearing all firmware state.
|
||||||
|
/// b. STOP + START UNIT (CDB 0x1B) — power-cycles the
|
||||||
|
/// drive's logical unit, like pressing the eject button
|
||||||
|
/// and reinserting.
|
||||||
|
/// 8. **close** — release the fd. Drive is clean, nobody holds it.
|
||||||
|
pub fn reset(device: &Path) -> Result<()> {
|
||||||
|
let c_path = Self::to_c_path(device);
|
||||||
|
|
||||||
|
// Step 1-2: open + close — flush stale kernel SG_IO state
|
||||||
|
let probe_fd = unsafe {
|
||||||
|
libc::open(
|
||||||
|
c_path.as_ptr() as *const libc::c_char,
|
||||||
|
libc::O_RDWR | libc::O_NONBLOCK,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if probe_fd >= 0 {
|
||||||
|
unsafe { libc::close(probe_fd) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: let drive settle
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||||
|
|
||||||
|
// Step 4: open clean fd
|
||||||
|
let fd = unsafe {
|
||||||
|
libc::open(
|
||||||
|
c_path.as_ptr() as *const libc::c_char,
|
||||||
|
libc::O_RDWR | libc::O_NONBLOCK,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if fd < 0 {
|
||||||
|
return Self::open_error(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: unlock tray
|
||||||
|
let _ = Self::raw_command(fd, &[0x1E, 0, 0, 0, 0, 0], 3_000);
|
||||||
|
|
||||||
|
// Step 6: TUR — if drive responds, we're done
|
||||||
|
if Self::raw_command(fd, &[0, 0, 0, 0, 0, 0], 3_000).is_err() {
|
||||||
|
// Step 7: escalate — SG_SCSI_RESET
|
||||||
|
let mut reset_type: i32 = SG_SCSI_RESET_DEVICE;
|
||||||
|
unsafe { libc::ioctl(fd, SG_SCSI_RESET as _, &mut reset_type) };
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||||
|
|
||||||
|
if Self::raw_command(fd, &[0, 0, 0, 0, 0, 0], 3_000).is_err() {
|
||||||
|
// STOP + START
|
||||||
|
let _ = Self::raw_command(fd, &[0x1B, 0, 0, 0, 0x00, 0], 3_000);
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
let _ = Self::raw_command(fd, &[0x1B, 0, 0, 0, 0x01, 0], 3_000);
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||||
|
let _ = Self::raw_command(fd, &[0, 0, 0, 0, 0, 0], 3_000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 8: close — drive is clean
|
||||||
|
unsafe { libc::close(fd) };
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_error<T>(device: &Path) -> Result<T> {
|
||||||
|
let err = std::io::Error::last_os_error();
|
||||||
|
Err(if err.kind() == std::io::ErrorKind::PermissionDenied {
|
||||||
|
Error::DevicePermission {
|
||||||
|
path: format!("{}: permission denied (try running as root)", device.display()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Error::DeviceNotFound {
|
||||||
|
path: device.display().to_string(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a raw SCSI command on an fd. Used by reset() before the
|
||||||
|
/// transport is constructed.
|
||||||
|
fn raw_command(fd: i32, cdb: &[u8], timeout_ms: u32) -> std::result::Result<(), ()> {
|
||||||
|
let mut sense = [0u8; 32];
|
||||||
|
let mut hdr: sg_io_hdr = unsafe { std::mem::zeroed() };
|
||||||
|
hdr.interface_id = b'S' as i32;
|
||||||
|
hdr.dxfer_direction = SG_DXFER_NONE;
|
||||||
|
hdr.cmd_len = cdb.len().min(16) as u8;
|
||||||
|
hdr.mx_sb_len = sense.len() as u8;
|
||||||
|
hdr.dxfer_len = 0;
|
||||||
|
hdr.dxferp = std::ptr::null_mut();
|
||||||
|
hdr.cmdp = cdb.as_ptr();
|
||||||
|
hdr.sbp = sense.as_mut_ptr();
|
||||||
|
hdr.timeout = timeout_ms;
|
||||||
|
hdr.flags = SG_FLAG_Q_AT_HEAD;
|
||||||
|
|
||||||
|
let ret = unsafe { libc::ioctl(fd, SG_IO as _, &mut hdr as *mut sg_io_hdr) };
|
||||||
|
if ret < 0 || hdr.status != 0 {
|
||||||
|
Err(())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_c_path(device: &Path) -> Vec<u8> {
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
let path_bytes = device.as_os_str().as_bytes();
|
||||||
|
let mut c_path = Vec::with_capacity(path_bytes.len() + 1);
|
||||||
|
c_path.extend_from_slice(path_bytes);
|
||||||
|
c_path.push(0);
|
||||||
|
c_path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve /dev/sr* -> /dev/sg* via sysfs. If already sg, returns as-is.
|
||||||
|
/// Falls back to the original path if resolution fails.
|
||||||
|
fn resolve_to_sg(device: &Path) -> std::path::PathBuf {
|
||||||
|
let dev_name = match device.file_name().and_then(|n| n.to_str()) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => return device.to_path_buf(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if dev_name.starts_with("sg") {
|
||||||
|
return device.to_path_buf();
|
||||||
|
}
|
||||||
|
|
||||||
|
if dev_name.starts_with("sr") {
|
||||||
|
let sg_dir = format!("/sys/class/block/{}/device/scsi_generic", dev_name);
|
||||||
|
if let Ok(mut entries) = std::fs::read_dir(&sg_dir) {
|
||||||
|
if let Some(Ok(entry)) = entries.next() {
|
||||||
|
let sg_name = entry.file_name();
|
||||||
|
return std::path::PathBuf::from(format!(
|
||||||
|
"/dev/{}",
|
||||||
|
sg_name.to_string_lossy()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
device.to_path_buf()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for SgIoTransport {
|
impl Drop for SgIoTransport {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
// Unlock tray before closing — don't leave it locked
|
||||||
libc::close(self.fd);
|
let _ = Self::raw_command(self.fd, &[0x1E, 0, 0, 0, 0, 0], 3_000);
|
||||||
}
|
unsafe { libc::close(self.fd) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +274,14 @@ impl ScsiTransport for SgIoTransport {
|
|||||||
hdr.cmdp = cdb.as_ptr();
|
hdr.cmdp = cdb.as_ptr();
|
||||||
hdr.sbp = sense.as_mut_ptr();
|
hdr.sbp = sense.as_mut_ptr();
|
||||||
hdr.timeout = timeout_ms;
|
hdr.timeout = timeout_ms;
|
||||||
|
if dxfer_direction == SG_DXFER_FROM_DEV
|
||||||
|
&& data.len() >= 4096
|
||||||
|
&& (data.as_ptr() as usize) % 4096 == 0
|
||||||
|
{
|
||||||
|
hdr.flags = SG_FLAG_DIRECT_IO | SG_FLAG_Q_AT_HEAD;
|
||||||
|
} else {
|
||||||
|
hdr.flags = SG_FLAG_Q_AT_HEAD;
|
||||||
|
}
|
||||||
|
|
||||||
let ret = unsafe { libc::ioctl(self.fd, SG_IO as _, &mut hdr as *mut sg_io_hdr) };
|
let ret = unsafe { libc::ioctl(self.fd, SG_IO as _, &mut hdr as *mut sg_io_hdr) };
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,24 @@ impl MacScsiTransport {
|
|||||||
exclusive: true,
|
exclusive: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset the drive to a known good state.
|
||||||
|
/// On macOS, we open the device, release exclusive access, wait for
|
||||||
|
/// the system to reclaim it, then the next open() re-acquires.
|
||||||
|
/// IOKit's USB layer handles device-level resets internally when the
|
||||||
|
/// exclusive access is released and re-acquired.
|
||||||
|
///
|
||||||
|
/// NOTE: untested — macOS reset may need IOUSBDeviceInterface::ResetDevice()
|
||||||
|
/// for USB drives. This is a best-effort implementation.
|
||||||
|
pub fn reset(device: &Path) -> Result<()> {
|
||||||
|
// Opening and immediately dropping triggers release of exclusive access
|
||||||
|
// which forces IOKit to reset the device state.
|
||||||
|
if let Ok(transport) = Self::open(device) {
|
||||||
|
drop(transport); // Drop releases exclusive access + closes plugin
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for MacScsiTransport {
|
impl Drop for MacScsiTransport {
|
||||||
|
|||||||
+26
-1
@@ -60,7 +60,7 @@ pub trait ScsiTransport: Send {
|
|||||||
) -> Result<ScsiResult>;
|
) -> Result<ScsiResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Platform-agnostic open ──────────────────────────────────────────────────
|
// ── Platform-agnostic open / reset ──────────────────────────────────────────
|
||||||
|
|
||||||
/// Open a SCSI transport for the given device path.
|
/// Open a SCSI transport for the given device path.
|
||||||
/// Selects the right backend for the current platform.
|
/// Selects the right backend for the current platform.
|
||||||
@@ -88,6 +88,31 @@ pub fn open(device: &Path) -> Result<Box<dyn ScsiTransport>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset a SCSI device to a known good state. Platform-specific.
|
||||||
|
/// On Linux: open/close fd cycle + TUR + SG_SCSI_RESET escalation.
|
||||||
|
pub fn reset(device: &Path) -> Result<()> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
linux::SgIoTransport::reset(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
macos::MacScsiTransport::reset(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
windows::SptiTransport::reset(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
|
||||||
|
{
|
||||||
|
let _ = device;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── CDB builders (platform-agnostic) ────────────────────────────────────────
|
// ── CDB builders (platform-agnostic) ────────────────────────────────────────
|
||||||
|
|
||||||
/// SCSI INQUIRY response.
|
/// SCSI INQUIRY response.
|
||||||
|
|||||||
@@ -136,6 +136,55 @@ impl SptiTransport {
|
|||||||
|
|
||||||
Ok(SptiTransport { handle })
|
Ok(SptiTransport { handle })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reset the drive to a known good state.
|
||||||
|
/// Opens the device, sends IOCTL_STORAGE_RESET_DEVICE to reset
|
||||||
|
/// the USB/SCSI bus, then closes. Same concept as SG_SCSI_RESET on Linux.
|
||||||
|
pub fn reset(device: &Path) -> Result<()> {
|
||||||
|
const IOCTL_STORAGE_RESET_DEVICE: u32 = 0x002D1004;
|
||||||
|
|
||||||
|
let dev_str = device.to_str().ok_or_else(|| Error::DeviceNotFound {
|
||||||
|
path: device.display().to_string(),
|
||||||
|
})?;
|
||||||
|
let win_path = normalize_device_path(dev_str);
|
||||||
|
let wide: Vec<u16> = win_path.encode_utf16().chain(std::iter::once(0)).collect();
|
||||||
|
|
||||||
|
// Open
|
||||||
|
let handle = unsafe {
|
||||||
|
CreateFileW(
|
||||||
|
wide.as_ptr(),
|
||||||
|
GENERIC_READ | GENERIC_WRITE,
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||||
|
std::ptr::null(),
|
||||||
|
OPEN_EXISTING,
|
||||||
|
FILE_ATTRIBUTE_NORMAL,
|
||||||
|
std::ptr::null(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if handle == INVALID_HANDLE_VALUE {
|
||||||
|
return Ok(()); // can't open — skip reset, not fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send device reset
|
||||||
|
let mut returned: u32 = 0;
|
||||||
|
unsafe {
|
||||||
|
DeviceIoControl(
|
||||||
|
handle,
|
||||||
|
IOCTL_STORAGE_RESET_DEVICE,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
&mut returned,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close and wait for drive to settle
|
||||||
|
unsafe { CloseHandle(handle) };
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for SptiTransport {
|
impl Drop for SptiTransport {
|
||||||
|
|||||||
Reference in New Issue
Block a user