#[cfg(all(unix, feature = "libusb"))]
use super::disk::DiskImage;
#[cfg(all(unix, feature = "libusb"))]
use crate::server::usb::descriptors::{
scsi_opcode, scsi_sense_key, CommandBlockWrapper, CommandStatusWrapper,
};
#[cfg(all(unix, feature = "libusb"))]
use anyhow::Result;
#[cfg(all(unix, feature = "libusb"))]
use std::sync::{Arc, Mutex};
#[cfg(all(unix, feature = "libusb"))]
use tokio::sync::mpsc;
#[cfg(all(unix, feature = "libusb"))]
use tracing::{debug, error, info, trace, warn};
#[cfg(all(unix, feature = "libusb"))]
pub type SharedDisk = Arc<Mutex<DiskImage>>;
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MscIoEvent {
Read {
lba: u32,
sectors: u32,
bytes: usize,
},
Write {
lba: u32,
sectors: u32,
bytes: usize,
},
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug)]
pub struct UsbMscHandler {
disk_image: SharedDisk,
medium_present: bool,
current_cbw: Option<CommandBlockWrapper>,
pending_data: Vec<u8>,
last_tag: u32,
expected_transfer: u32,
transferred: u32,
csw_pending: bool,
command_status: u8,
sense_key: u8,
sense_asc: u8,
sense_ascq: u8,
pending_write: Option<(u32, u32)>,
write_protect: bool,
io_tx: Option<mpsc::UnboundedSender<MscIoEvent>>,
}
#[cfg(all(unix, feature = "libusb"))]
impl UsbMscHandler {
pub fn new(disk_image: SharedDisk, write_protect: bool) -> Self {
Self {
disk_image,
medium_present: true,
current_cbw: None,
pending_data: Vec::new(),
last_tag: 0,
expected_transfer: 0,
transferred: 0,
csw_pending: false,
command_status: CommandStatusWrapper::STATUS_PASSED,
sense_key: scsi_sense_key::NO_SENSE,
sense_asc: 0,
sense_ascq: 0,
pending_write: None,
write_protect,
io_tx: None,
}
}
pub fn with_io_events(mut self, io_tx: mpsc::UnboundedSender<MscIoEvent>) -> Self {
self.io_tx = Some(io_tx);
self
}
fn report_io(&self, event: MscIoEvent) {
if let Some(tx) = &self.io_tx {
let _ = tx.send(event);
}
}
fn set_sense(&mut self, key: u8, asc: u8, ascq: u8) {
self.sense_key = key;
self.sense_asc = asc;
self.sense_ascq = ascq;
debug!(
"SCSI sense set: key={:#04x}, asc={:#04x}, ascq={:#04x}",
key, asc, ascq
);
}
fn clear_sense(&mut self) {
self.sense_key = scsi_sense_key::NO_SENSE;
self.sense_asc = 0;
self.sense_ascq = 0;
}
pub fn set_write_protect(&mut self, enabled: bool) {
info!(
"USB MSC: Write protection {}",
if enabled { "enabled" } else { "disabled" }
);
self.write_protect = enabled;
}
pub fn is_write_protected(&self) -> bool {
self.write_protect
}
pub fn mount_disk(&mut self, new_disk: SharedDisk) {
info!("USB MSC: Mounting new disk image");
self.disk_image = new_disk;
self.medium_present = true;
self.reset_bot_state();
}
pub fn eject_disk(&mut self) {
info!("USB MSC: Ejecting disk");
self.medium_present = false;
self.set_sense(scsi_sense_key::NOT_READY, 0x3A, 0x00); }
pub fn is_medium_present(&self) -> bool {
self.medium_present
}
pub fn get_disk_info(&self) -> Option<(u32, u32)> {
let disk = self.disk_image.lock().ok()?;
Some((disk.total_sectors(), disk.bytes_per_sector()))
}
fn reset_bot_state(&mut self) {
debug!("Resetting BOT state");
self.current_cbw = None;
self.pending_data.clear();
self.csw_pending = false;
self.pending_write = None;
self.expected_transfer = 0;
self.transferred = 0;
self.command_status = CommandStatusWrapper::STATUS_PASSED;
self.clear_sense();
}
fn no_medium(&mut self) -> u8 {
self.set_sense(scsi_sense_key::NOT_READY, 0x3A, 0x00);
CommandStatusWrapper::STATUS_FAILED
}
fn malformed_cdb(&mut self, opcode: u8, need: usize, got: usize) -> u8 {
warn!(
"SCSI command {:#04x} needs {} CDB bytes, got {}",
opcode, need, got
);
self.set_sense(scsi_sense_key::ILLEGAL_REQUEST, 0x24, 0x00); CommandStatusWrapper::STATUS_FAILED
}
fn handle_scsi_command(&mut self, cmd: &[u8]) -> u8 {
let Some(&opcode) = cmd.first() else {
warn!("Empty SCSI command block");
self.set_sense(scsi_sense_key::ILLEGAL_REQUEST, 0x20, 0x00);
return CommandStatusWrapper::STATUS_FAILED;
};
trace!("SCSI command: opcode={:#04x} ({} bytes)", opcode, cmd.len());
match opcode {
scsi_opcode::INQUIRY => self.scsi_inquiry(cmd),
scsi_opcode::TEST_UNIT_READY => self.scsi_test_unit_ready(),
scsi_opcode::READ_CAPACITY_10 => self.scsi_read_capacity(),
scsi_opcode::READ_10 => self.scsi_read10(cmd),
scsi_opcode::WRITE_10 => self.scsi_write10(cmd),
scsi_opcode::REQUEST_SENSE => self.scsi_request_sense(),
scsi_opcode::MODE_SENSE_6 => self.scsi_mode_sense(cmd),
scsi_opcode::PREVENT_ALLOW_MEDIUM_REMOVAL => self.scsi_prevent_allow_removal(),
scsi_opcode::READ_FORMAT_CAPACITIES => self.scsi_read_format_capacities(),
_ => {
warn!("Unsupported SCSI command: {:#04x}", opcode);
self.set_sense(scsi_sense_key::ILLEGAL_REQUEST, 0x20, 0x00);
CommandStatusWrapper::STATUS_FAILED
}
}
}
fn scsi_inquiry(&mut self, cmd: &[u8]) -> u8 {
let Some(&alloc_len) = cmd.get(4) else {
return self.malformed_cdb(scsi_opcode::INQUIRY, 5, cmd.len());
};
let alloc_len = alloc_len as usize;
debug!("SCSI INQUIRY (alloc_len={})", alloc_len);
#[rustfmt::skip]
let response = vec![
0x00, 0x80, 0x05, 0x02, 0x1F, 0x00, 0x00, 0x00,
b'N', b'e', b't', b'G', b'e', b't', b' ', b' ',
b'V', b'i', b'r', b't', b'u', b'a', b'l', b' ',
b'D', b'i', b's', b'k', b' ', b' ', b' ', b' ',
b'1', b'.', b'0', b' ',
];
self.pending_data = response[..alloc_len.min(response.len())].to_vec();
self.clear_sense();
CommandStatusWrapper::STATUS_PASSED
}
fn scsi_test_unit_ready(&mut self) -> u8 {
debug!("SCSI TEST_UNIT_READY");
if !self.medium_present {
return self.no_medium();
}
self.clear_sense();
CommandStatusWrapper::STATUS_PASSED
}
fn scsi_read_capacity(&mut self) -> u8 {
if !self.medium_present {
return self.no_medium();
}
let Some((total_sectors, block_size)) = self.get_disk_info() else {
error!("READ_CAPACITY: disk image mutex poisoned");
self.set_sense(scsi_sense_key::HARDWARE_ERROR, 0x44, 0x00);
return CommandStatusWrapper::STATUS_FAILED;
};
let last_lba = total_sectors.saturating_sub(1);
debug!(
"SCSI READ_CAPACITY: last_lba={}, block_size={}",
last_lba, block_size
);
let mut response = Vec::with_capacity(8);
response.extend_from_slice(&last_lba.to_be_bytes());
response.extend_from_slice(&block_size.to_be_bytes());
self.pending_data = response;
self.clear_sense();
CommandStatusWrapper::STATUS_PASSED
}
fn scsi_read10(&mut self, cmd: &[u8]) -> u8 {
if cmd.len() < 9 {
return self.malformed_cdb(scsi_opcode::READ_10, 9, cmd.len());
}
if !self.medium_present {
return self.no_medium();
}
let lba = u32::from_be_bytes([cmd[2], cmd[3], cmd[4], cmd[5]]);
let transfer_len = u16::from_be_bytes([cmd[7], cmd[8]]) as u32;
debug!(
"SCSI READ(10): lba={}, transfer_len={} sectors",
lba, transfer_len
);
let result = match self.disk_image.lock() {
Ok(disk) => disk.read_sectors(lba, transfer_len),
Err(_) => Err(anyhow::anyhow!("disk image mutex poisoned")),
};
match result {
Ok(data) => {
let bytes = data.len();
self.pending_data = data;
self.clear_sense();
self.report_io(MscIoEvent::Read {
lba,
sectors: transfer_len,
bytes,
});
CommandStatusWrapper::STATUS_PASSED
}
Err(e) => {
error!("READ(10) failed: {}", e);
self.set_sense(scsi_sense_key::ILLEGAL_REQUEST, 0x21, 0x00);
CommandStatusWrapper::STATUS_FAILED
}
}
}
fn scsi_write10(&mut self, cmd: &[u8]) -> u8 {
if cmd.len() < 9 {
return self.malformed_cdb(scsi_opcode::WRITE_10, 9, cmd.len());
}
if !self.medium_present {
return self.no_medium();
}
let lba = u32::from_be_bytes([cmd[2], cmd[3], cmd[4], cmd[5]]);
let transfer_len = u16::from_be_bytes([cmd[7], cmd[8]]) as u32;
debug!(
"SCSI WRITE(10): lba={}, transfer_len={} sectors (write_protect={})",
lba, transfer_len, self.write_protect
);
if self.write_protect {
warn!("WRITE(10) blocked: disk is write-protected");
self.set_sense(scsi_sense_key::DATA_PROTECT, 0x27, 0x00);
return CommandStatusWrapper::STATUS_FAILED;
}
self.pending_write = Some((lba, transfer_len));
self.clear_sense();
CommandStatusWrapper::STATUS_PASSED
}
fn scsi_request_sense(&mut self) -> u8 {
debug!(
"SCSI REQUEST_SENSE: key={:#04x}, asc={:#04x}, ascq={:#04x}",
self.sense_key, self.sense_asc, self.sense_ascq
);
#[rustfmt::skip]
let response = vec![
0x70, 0x00,
self.sense_key, 0x00, 0x00, 0x00, 0x00,
0x0A, 0x00, 0x00, 0x00, 0x00,
self.sense_asc, self.sense_ascq, 0x00, 0x00, 0x00, 0x00,
];
self.pending_data = response;
self.clear_sense();
if !self.medium_present {
self.set_sense(scsi_sense_key::NOT_READY, 0x3A, 0x00);
}
CommandStatusWrapper::STATUS_PASSED
}
fn scsi_mode_sense(&mut self, cmd: &[u8]) -> u8 {
let page_code = cmd.get(2).map(|b| b & 0x3F).unwrap_or(0x3F);
debug!("SCSI MODE_SENSE(6): page_code={:#04x}", page_code);
#[rustfmt::skip]
let response = vec![
0x03, 0x00, if self.write_protect { 0x80 } else { 0x00 }, 0x00, ];
self.pending_data = response;
self.clear_sense();
CommandStatusWrapper::STATUS_PASSED
}
fn scsi_prevent_allow_removal(&mut self) -> u8 {
debug!("SCSI PREVENT_ALLOW_MEDIUM_REMOVAL");
self.clear_sense();
CommandStatusWrapper::STATUS_PASSED
}
fn scsi_read_format_capacities(&mut self) -> u8 {
if !self.medium_present {
return self.no_medium();
}
let Some((total_sectors, block_size)) = self.get_disk_info() else {
error!("READ_FORMAT_CAPACITIES: disk image mutex poisoned");
self.set_sense(scsi_sense_key::HARDWARE_ERROR, 0x44, 0x00);
return CommandStatusWrapper::STATUS_FAILED;
};
debug!("SCSI READ_FORMAT_CAPACITIES: {} sectors", total_sectors);
#[rustfmt::skip]
let mut response = vec![
0x00, 0x00, 0x00, 0x08, ];
response.extend_from_slice(&total_sectors.to_be_bytes());
response.push(0x02); response.extend_from_slice(&block_size.to_be_bytes()[1..]);
self.pending_data = response;
self.clear_sense();
CommandStatusWrapper::STATUS_PASSED
}
fn handle_write_data(&mut self, data: &[u8]) -> Result<()> {
let Some((lba, transfer_len)) = self.pending_write.take() else {
warn!("Received write data with no pending WRITE(10) command");
return Ok(());
};
debug!(
"Writing {} bytes to LBA {} ({} sectors expected)",
data.len(),
lba,
transfer_len
);
let result = match self.disk_image.lock() {
Ok(mut disk) => disk.write_sectors(lba, data),
Err(_) => Err(anyhow::anyhow!("disk image mutex poisoned")),
};
match result {
Ok(sectors_written) => {
info!(
"WRITE(10) completed: {} sectors written to LBA {}",
sectors_written, lba
);
self.report_io(MscIoEvent::Write {
lba,
sectors: sectors_written,
bytes: data.len(),
});
Ok(())
}
Err(e) => {
error!("WRITE(10) failed: {}", e);
self.set_sense(scsi_sense_key::MEDIUM_ERROR, 0x03, 0x00);
Err(e)
}
}
}
fn begin_command(&mut self, cbw: CommandBlockWrapper) {
let tag = cbw.tag;
let lun = cbw.lun;
let flags = cbw.flags;
let data_transfer_length = cbw.data_transfer_length;
debug!(
"BOT: Received CBW (tag={:#010x}, lun={}, flags={:#04x}, length={})",
tag, lun, flags, data_transfer_length
);
self.last_tag = tag;
self.expected_transfer = data_transfer_length;
self.transferred = 0;
self.pending_data.clear();
let scsi_cmd = cbw.scsi_command().to_vec();
self.command_status = self.handle_scsi_command(&scsi_cmd);
self.current_cbw = Some(cbw);
if self.pending_data.is_empty() && self.pending_write.is_none() {
self.csw_pending = true;
}
}
fn build_csw(&mut self) -> [u8; CommandStatusWrapper::SIZE] {
let residue = self.expected_transfer.saturating_sub(self.transferred);
let csw = CommandStatusWrapper::new(self.last_tag, residue, self.command_status);
let tag = csw.tag;
let status = csw.status;
debug!(
"BOT: Sending CSW (tag={:#010x}, status={}, residue={})",
tag, status, residue
);
self.csw_pending = false;
self.current_cbw = None;
self.expected_transfer = 0;
self.transferred = 0;
self.command_status = CommandStatusWrapper::STATUS_PASSED;
csw.to_bytes()
}
}
#[cfg(all(unix, feature = "libusb"))]
impl usbip::UsbInterfaceHandler for UsbMscHandler {
fn handle_urb(
&mut self,
_interface: &usbip::UsbInterface,
endpoint: usbip::UsbEndpoint,
transfer_buffer_length: u32,
setup: usbip::SetupPacket,
data: &[u8],
) -> std::result::Result<Vec<u8>, std::io::Error> {
if endpoint.is_ep0() {
trace!(
"MSC control request: type={:#04x}, request={:#04x}",
setup.request_type,
setup.request
);
return match (setup.request_type, setup.request) {
(0x21, 0xFF) => {
debug!("BOT: Mass Storage Reset");
self.reset_bot_state();
Ok(vec![])
}
(0xA1, 0xFE) => {
debug!("BOT: Get Max LUN");
Ok(vec![0x00]) }
_ => {
warn!(
"Unsupported MSC control request: type={:#04x}, request={:#04x}",
setup.request_type, setup.request
);
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Unsupported control request",
))
}
};
}
if matches!(endpoint.direction(), usbip::Direction::Out) {
if self.pending_write.is_some() {
if let Err(e) = self.handle_write_data(data) {
error!("Failed to handle write data: {}", e);
self.command_status = CommandStatusWrapper::STATUS_FAILED;
} else {
self.transferred = self
.transferred
.saturating_add(u32::try_from(data.len()).unwrap_or(u32::MAX));
}
self.csw_pending = true;
return Ok(vec![]);
}
if self.csw_pending {
debug!(
"BOT: discarding {} byte(s) of data for a command that already completed",
data.len()
);
return Ok(vec![]);
}
return match CommandBlockWrapper::parse(data) {
Some(cbw) => {
self.begin_command(cbw);
Ok(vec![])
}
None => {
error!(
"Failed to parse CBW: invalid format ({} bytes on the bulk OUT endpoint)",
data.len()
);
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid CBW format",
))
}
};
}
if !self.pending_data.is_empty() {
let take = if transfer_buffer_length == 0 {
self.pending_data.len()
} else {
(transfer_buffer_length as usize).min(self.pending_data.len())
};
let chunk: Vec<u8> = self.pending_data.drain(..take).collect();
debug!(
"BOT: Sending {} bytes of data ({} still queued)",
chunk.len(),
self.pending_data.len()
);
self.transferred = self
.transferred
.saturating_add(u32::try_from(chunk.len()).unwrap_or(u32::MAX));
if self.pending_data.is_empty() {
self.csw_pending = true;
}
return Ok(chunk);
}
if self.csw_pending {
return Ok(self.build_csw().to_vec());
}
Ok(vec![])
}
fn get_class_specific_descriptor(&self) -> Vec<u8> {
vec![]
}
fn as_any(&mut self) -> &mut dyn std::any::Any {
self
}
}