Restructure: scsi/ and aacs/ as module directories
- scsi.rs → scsi/mod.rs (trait, constants, CDB builders) + scsi/linux.rs (SG_IO) Ready for scsi/macos.rs and scsi/windows.rs when needed. - aacs.rs + aacs_handshake.rs → aacs/mod.rs (keys, decrypt) + aacs/handshake.rs (SCSI auth) - All internal refs use super:: within module, crate:: across modules - Zero warnings, 31 tests passing
This commit is contained in:
@@ -670,8 +670,8 @@ pub fn read_data_keys(session: &mut DriveSession, auth: &mut AacsAuth) -> Result
|
|||||||
enc_wdk.copy_from_slice(&response[20..36]);
|
enc_wdk.copy_from_slice(&response[20..36]);
|
||||||
|
|
||||||
// Decrypt with bus key (AES-ECB)
|
// Decrypt with bus key (AES-ECB)
|
||||||
let read_data_key = crate::aacs::aes_ecb_decrypt(&auth.bus_key, &enc_rdk);
|
let read_data_key = super::aes_ecb_decrypt(&auth.bus_key, &enc_rdk);
|
||||||
let write_data_key = crate::aacs::aes_ecb_decrypt(&auth.bus_key, &enc_wdk);
|
let write_data_key = super::aes_ecb_decrypt(&auth.bus_key, &enc_wdk);
|
||||||
|
|
||||||
auth.read_data_key = Some(read_data_key);
|
auth.read_data_key = Some(read_data_key);
|
||||||
Ok((read_data_key, write_data_key))
|
Ok((read_data_key, write_data_key))
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
//! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc.
|
//! The VUK decrypts title keys from AACS/Unit_Key_RO.inf on disc.
|
||||||
//! Title keys decrypt m2ts stream content (AES-128-CBC).
|
//! Title keys decrypt m2ts stream content (AES-128-CBC).
|
||||||
|
|
||||||
|
pub mod handshake;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
use aes::cipher::{BlockEncrypt, BlockDecrypt, KeyInit, generic_array::GenericArray};
|
use aes::cipher::{BlockEncrypt, BlockDecrypt, KeyInit, generic_array::GenericArray};
|
||||||
@@ -49,7 +49,7 @@ fn main() {
|
|||||||
// AACS handshake
|
// AACS handshake
|
||||||
println!();
|
println!();
|
||||||
print!("AACS authenticate... ");
|
print!("AACS authenticate... ");
|
||||||
let mut auth = match libfreemkv::aacs_handshake::aacs_authenticate(
|
let mut auth = match libfreemkv::aacs::handshake::aacs_authenticate(
|
||||||
&mut session,
|
&mut session,
|
||||||
&host_cert.private_key,
|
&host_cert.private_key,
|
||||||
&host_cert.certificate,
|
&host_cert.certificate,
|
||||||
@@ -69,7 +69,7 @@ fn main() {
|
|||||||
|
|
||||||
// Read Volume ID
|
// Read Volume ID
|
||||||
print!("Reading Volume ID... ");
|
print!("Reading Volume ID... ");
|
||||||
match libfreemkv::aacs_handshake::read_volume_id(&mut session, &mut auth) {
|
match libfreemkv::aacs::handshake::read_volume_id(&mut session, &mut auth) {
|
||||||
Ok(vid) => {
|
Ok(vid) => {
|
||||||
println!("OK");
|
println!("OK");
|
||||||
println!(" VID: {:02x?}", vid);
|
println!(" VID: {:02x?}", vid);
|
||||||
@@ -91,7 +91,7 @@ fn main() {
|
|||||||
|
|
||||||
// Read data keys (AACS 2.0)
|
// Read data keys (AACS 2.0)
|
||||||
print!("Reading data keys... ");
|
print!("Reading data keys... ");
|
||||||
match libfreemkv::aacs_handshake::read_data_keys(&mut session, &mut auth) {
|
match libfreemkv::aacs::handshake::read_data_keys(&mut session, &mut auth) {
|
||||||
Ok((rdk, wdk)) => {
|
Ok((rdk, wdk)) => {
|
||||||
println!("OK (AACS 2.0 bus encryption)");
|
println!("OK (AACS 2.0 bus encryption)");
|
||||||
println!(" Read data key: {:02x?}", rdk);
|
println!(" Read data key: {:02x?}", rdk);
|
||||||
|
|||||||
+4
-4
@@ -426,7 +426,7 @@ impl Disc {
|
|||||||
keydb_path: &std::path::Path,
|
keydb_path: &std::path::Path,
|
||||||
) -> Result<AacsState> {
|
) -> Result<AacsState> {
|
||||||
use crate::aacs::{self, KeyDb};
|
use crate::aacs::{self, KeyDb};
|
||||||
use crate::aacs_handshake;
|
use crate::aacs::handshake;
|
||||||
|
|
||||||
// Load KEYDB
|
// Load KEYDB
|
||||||
let keydb = KeyDb::load(keydb_path).map_err(|e| Error::AacsError {
|
let keydb = KeyDb::load(keydb_path).map_err(|e| Error::AacsError {
|
||||||
@@ -444,11 +444,11 @@ impl Disc {
|
|||||||
if !device_path.is_empty() {
|
if !device_path.is_empty() {
|
||||||
if let Ok(mut aacs_session) = DriveSession::open_no_unlock(std::path::Path::new(&device_path)) {
|
if let Ok(mut aacs_session) = DriveSession::open_no_unlock(std::path::Path::new(&device_path)) {
|
||||||
if let Ok(hc) = keydb.host_cert.as_ref().ok_or(()) {
|
if let Ok(hc) = keydb.host_cert.as_ref().ok_or(()) {
|
||||||
if let Ok(mut auth) = aacs_handshake::aacs_authenticate(
|
if let Ok(mut auth) = handshake::aacs_authenticate(
|
||||||
&mut aacs_session, &hc.private_key, &hc.certificate,
|
&mut aacs_session, &hc.private_key, &hc.certificate,
|
||||||
) {
|
) {
|
||||||
vid = aacs_handshake::read_volume_id(&mut aacs_session, &mut auth).ok();
|
vid = handshake::read_volume_id(&mut aacs_session, &mut auth).ok();
|
||||||
read_data_key = aacs_handshake::read_data_keys(&mut aacs_session, &mut auth)
|
read_data_key = handshake::read_data_keys(&mut aacs_session, &mut auth)
|
||||||
.ok().map(|(rdk, _)| rdk);
|
.ok().map(|(rdk, _)| rdk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ pub mod clpi;
|
|||||||
pub mod disc;
|
pub mod disc;
|
||||||
pub mod jar;
|
pub mod jar;
|
||||||
pub mod aacs;
|
pub mod aacs;
|
||||||
pub mod aacs_handshake;
|
|
||||||
|
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
pub use drive::DriveSession;
|
pub use drive::DriveSession;
|
||||||
|
|||||||
-264
@@ -1,264 +0,0 @@
|
|||||||
//! SCSI/MMC command interface.
|
|
||||||
//!
|
|
||||||
//! Platform backends:
|
|
||||||
//! - Linux: SG_IO ioctl
|
|
||||||
//! - macOS: IOKit SCSI passthrough (planned)
|
|
||||||
//! - Windows: SPTI (planned)
|
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
// ── SCSI opcodes (SPC-4, MMC-6) ────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub const SCSI_INQUIRY: u8 = 0x12;
|
|
||||||
pub const SCSI_READ_CAPACITY: u8 = 0x25;
|
|
||||||
pub const SCSI_READ_10: u8 = 0x28;
|
|
||||||
pub const SCSI_READ_BUFFER: u8 = 0x3C;
|
|
||||||
pub const SCSI_READ_TOC: u8 = 0x43;
|
|
||||||
pub const SCSI_GET_CONFIGURATION: u8 = 0x46;
|
|
||||||
pub const SCSI_SEND_KEY: u8 = 0xA3;
|
|
||||||
pub const SCSI_REPORT_KEY: u8 = 0xA4;
|
|
||||||
pub const SCSI_READ_12: u8 = 0xA8;
|
|
||||||
pub const SCSI_READ_DISC_STRUCTURE: u8 = 0xAD;
|
|
||||||
|
|
||||||
/// AACS key class for REPORT KEY / SEND KEY commands.
|
|
||||||
pub const AACS_KEY_CLASS: u8 = 0x02;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum DataDirection {
|
|
||||||
None,
|
|
||||||
FromDevice,
|
|
||||||
ToDevice,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ScsiResult {
|
|
||||||
pub status: u8,
|
|
||||||
pub bytes_transferred: usize,
|
|
||||||
pub sense: [u8; 32],
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Low-level SCSI transport — implemented per platform.
|
|
||||||
pub trait ScsiTransport {
|
|
||||||
fn execute(
|
|
||||||
&mut self,
|
|
||||||
cdb: &[u8],
|
|
||||||
direction: DataDirection,
|
|
||||||
data: &mut [u8],
|
|
||||||
timeout_ms: u32,
|
|
||||||
) -> Result<ScsiResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Linux: SG_IO ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
const SG_IO: u32 = 0x2285;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
const SG_DXFER_NONE: i32 = -1;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
const SG_DXFER_TO_DEV: i32 = -2;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
const SG_DXFER_FROM_DEV: i32 = -3;
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[repr(C)]
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
struct sg_io_hdr {
|
|
||||||
interface_id: i32,
|
|
||||||
dxfer_direction: i32,
|
|
||||||
cmd_len: u8,
|
|
||||||
mx_sb_len: u8,
|
|
||||||
iovec_count: u16,
|
|
||||||
dxfer_len: u32,
|
|
||||||
dxferp: *mut u8,
|
|
||||||
cmdp: *const u8,
|
|
||||||
sbp: *mut u8,
|
|
||||||
timeout: u32,
|
|
||||||
flags: u32,
|
|
||||||
pack_id: i32,
|
|
||||||
usr_ptr: *mut libc::c_void,
|
|
||||||
status: u8,
|
|
||||||
masked_status: u8,
|
|
||||||
msg_status: u8,
|
|
||||||
sb_len_wr: u8,
|
|
||||||
host_status: u16,
|
|
||||||
driver_status: u16,
|
|
||||||
resid: i32,
|
|
||||||
duration: u32,
|
|
||||||
info: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub struct SgIoTransport {
|
|
||||||
fd: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
impl SgIoTransport {
|
|
||||||
pub fn open(device: &Path) -> Result<Self> {
|
|
||||||
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);
|
|
||||||
|
|
||||||
let fd = unsafe { libc::open(c_path.as_ptr() as *const libc::c_char, libc::O_RDWR | libc::O_NONBLOCK) };
|
|
||||||
if fd < 0 {
|
|
||||||
return Err(Error::DeviceNotFound { path: device.display().to_string() });
|
|
||||||
}
|
|
||||||
Ok(SgIoTransport { fd })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
impl Drop for SgIoTransport {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
unsafe { libc::close(self.fd); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
impl ScsiTransport for SgIoTransport {
|
|
||||||
fn execute(
|
|
||||||
&mut self,
|
|
||||||
cdb: &[u8],
|
|
||||||
direction: DataDirection,
|
|
||||||
data: &mut [u8],
|
|
||||||
timeout_ms: u32,
|
|
||||||
) -> Result<ScsiResult> {
|
|
||||||
let mut sense = [0u8; 32];
|
|
||||||
|
|
||||||
let dxfer_direction = match direction {
|
|
||||||
DataDirection::None => SG_DXFER_NONE,
|
|
||||||
DataDirection::FromDevice => SG_DXFER_FROM_DEV,
|
|
||||||
DataDirection::ToDevice => SG_DXFER_TO_DEV,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut hdr: sg_io_hdr = unsafe { std::mem::zeroed() };
|
|
||||||
hdr.interface_id = b'S' as i32;
|
|
||||||
hdr.dxfer_direction = dxfer_direction;
|
|
||||||
hdr.cmd_len = cdb.len() as u8;
|
|
||||||
hdr.mx_sb_len = sense.len() as u8;
|
|
||||||
hdr.dxfer_len = data.len() as u32;
|
|
||||||
hdr.dxferp = data.as_mut_ptr();
|
|
||||||
hdr.cmdp = cdb.as_ptr();
|
|
||||||
hdr.sbp = sense.as_mut_ptr();
|
|
||||||
hdr.timeout = timeout_ms;
|
|
||||||
|
|
||||||
let ret = unsafe {
|
|
||||||
libc::ioctl(self.fd, SG_IO as _, &mut hdr as *mut sg_io_hdr)
|
|
||||||
};
|
|
||||||
|
|
||||||
if ret < 0 {
|
|
||||||
return Err(Error::IoError { source: std::io::Error::last_os_error() });
|
|
||||||
}
|
|
||||||
|
|
||||||
let bytes_transferred = (data.len() as i32 - hdr.resid) as usize;
|
|
||||||
|
|
||||||
if hdr.status != 0 {
|
|
||||||
let sense_key = if hdr.sb_len_wr > 2 { sense[2] & 0x0F } else { 0 };
|
|
||||||
return Err(Error::ScsiError {
|
|
||||||
opcode: cdb[0],
|
|
||||||
status: hdr.status,
|
|
||||||
sense_key,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ScsiResult {
|
|
||||||
status: hdr.status,
|
|
||||||
bytes_transferred,
|
|
||||||
sense,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── macOS: IOKit (planned) ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// TODO: IOKit MMC SCSI passthrough
|
|
||||||
// Use IOSCSIPeripheralDeviceType05 (MMC device nub)
|
|
||||||
// Send SCSITaskInterface commands via IOKit user client
|
|
||||||
|
|
||||||
// ─── Windows: SPTI (planned) ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// TODO: SCSI Pass Through Interface
|
|
||||||
// Use CreateFile on \\.\CdRomN
|
|
||||||
// Send IOCTL_SCSI_PASS_THROUGH_DIRECT
|
|
||||||
|
|
||||||
// ─── Platform-agnostic open ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Open a SCSI transport for the given device path.
|
|
||||||
pub fn open(device: &Path) -> Result<Box<dyn ScsiTransport>> {
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{ Ok(Box::new(SgIoTransport::open(device)?)) }
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
{ Err(Error::DeviceNotFound { path: format!("{}: platform not yet supported (Linux only)", device.display()) }) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── CDB builders (platform-agnostic) ───────────────────────────────────────
|
|
||||||
|
|
||||||
/// SCSI INQUIRY response.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct InquiryResult {
|
|
||||||
pub vendor_id: String,
|
|
||||||
pub model: String,
|
|
||||||
pub firmware: String,
|
|
||||||
pub raw: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send INQUIRY command and parse the standard response fields.
|
|
||||||
pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result<InquiryResult> {
|
|
||||||
let cdb = [0x12, 0x00, 0x00, 0x00, 0x60, 0x00];
|
|
||||||
let mut buf = [0u8; 96];
|
|
||||||
scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?;
|
|
||||||
|
|
||||||
let vendor = String::from_utf8_lossy(&buf[8..16]).trim().to_string();
|
|
||||||
let model = String::from_utf8_lossy(&buf[16..32]).trim().to_string();
|
|
||||||
let firmware = String::from_utf8_lossy(&buf[32..36]).trim().to_string();
|
|
||||||
|
|
||||||
Ok(InquiryResult {
|
|
||||||
vendor_id: vendor,
|
|
||||||
model,
|
|
||||||
firmware,
|
|
||||||
raw: buf.to_vec(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send GET CONFIGURATION for feature 0x010C (Firmware Information).
|
|
||||||
pub fn get_config_010c(scsi: &mut dyn ScsiTransport) -> Result<Vec<u8>> {
|
|
||||||
let cdb = [0x46, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00];
|
|
||||||
let mut buf = [0u8; 16];
|
|
||||||
scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?;
|
|
||||||
Ok(buf.to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a READ BUFFER (0x3C) CDB.
|
|
||||||
pub fn build_read_buffer(mode: u8, buffer_id: u8, offset: u32, length: u32) -> [u8; 10] {
|
|
||||||
[
|
|
||||||
0x3C, mode, buffer_id,
|
|
||||||
(offset >> 16) as u8, (offset >> 8) as u8, offset as u8,
|
|
||||||
(length >> 16) as u8, (length >> 8) as u8, length as u8,
|
|
||||||
0x00,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a SET CD SPEED (0xBB) CDB.
|
|
||||||
pub fn build_set_cd_speed(read_speed: u16) -> [u8; 12] {
|
|
||||||
[
|
|
||||||
0xBB, 0x00,
|
|
||||||
(read_speed >> 8) as u8, read_speed as u8,
|
|
||||||
0xFF, 0xFF,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a READ(10) CDB with the raw read flag (0x08).
|
|
||||||
pub fn build_read10_raw(lba: u32, count: u16) -> [u8; 10] {
|
|
||||||
[
|
|
||||||
0x28, 0x08,
|
|
||||||
(lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8,
|
|
||||||
0x00,
|
|
||||||
(count >> 8) as u8, count as u8,
|
|
||||||
0x00,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
//! Linux SCSI transport via SG_IO ioctl.
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use super::{ScsiTransport, ScsiResult, DataDirection};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
const SG_IO: u32 = 0x2285;
|
||||||
|
const SG_DXFER_NONE: i32 = -1;
|
||||||
|
const SG_DXFER_TO_DEV: i32 = -2;
|
||||||
|
const SG_DXFER_FROM_DEV: i32 = -3;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
struct sg_io_hdr {
|
||||||
|
interface_id: i32,
|
||||||
|
dxfer_direction: i32,
|
||||||
|
cmd_len: u8,
|
||||||
|
mx_sb_len: u8,
|
||||||
|
iovec_count: u16,
|
||||||
|
dxfer_len: u32,
|
||||||
|
dxferp: *mut u8,
|
||||||
|
cmdp: *const u8,
|
||||||
|
sbp: *mut u8,
|
||||||
|
timeout: u32,
|
||||||
|
flags: u32,
|
||||||
|
pack_id: i32,
|
||||||
|
usr_ptr: *mut libc::c_void,
|
||||||
|
status: u8,
|
||||||
|
masked_status: u8,
|
||||||
|
msg_status: u8,
|
||||||
|
sb_len_wr: u8,
|
||||||
|
host_status: u16,
|
||||||
|
driver_status: u16,
|
||||||
|
resid: i32,
|
||||||
|
duration: u32,
|
||||||
|
info: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SgIoTransport {
|
||||||
|
fd: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SgIoTransport {
|
||||||
|
pub fn open(device: &Path) -> Result<Self> {
|
||||||
|
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);
|
||||||
|
|
||||||
|
let fd = unsafe {
|
||||||
|
libc::open(c_path.as_ptr() as *const libc::c_char, libc::O_RDWR | libc::O_NONBLOCK)
|
||||||
|
};
|
||||||
|
if fd < 0 {
|
||||||
|
return Err(Error::DeviceNotFound { path: device.display().to_string() });
|
||||||
|
}
|
||||||
|
Ok(SgIoTransport { fd })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SgIoTransport {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe { libc::close(self.fd); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScsiTransport for SgIoTransport {
|
||||||
|
fn execute(
|
||||||
|
&mut self,
|
||||||
|
cdb: &[u8],
|
||||||
|
direction: DataDirection,
|
||||||
|
data: &mut [u8],
|
||||||
|
timeout_ms: u32,
|
||||||
|
) -> Result<ScsiResult> {
|
||||||
|
let mut sense = [0u8; 32];
|
||||||
|
|
||||||
|
let dxfer_direction = match direction {
|
||||||
|
DataDirection::None => SG_DXFER_NONE,
|
||||||
|
DataDirection::FromDevice => SG_DXFER_FROM_DEV,
|
||||||
|
DataDirection::ToDevice => SG_DXFER_TO_DEV,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut hdr: sg_io_hdr = unsafe { std::mem::zeroed() };
|
||||||
|
hdr.interface_id = b'S' as i32;
|
||||||
|
hdr.dxfer_direction = dxfer_direction;
|
||||||
|
hdr.cmd_len = cdb.len() as u8;
|
||||||
|
hdr.mx_sb_len = sense.len() as u8;
|
||||||
|
hdr.dxfer_len = data.len() as u32;
|
||||||
|
hdr.dxferp = data.as_mut_ptr();
|
||||||
|
hdr.cmdp = cdb.as_ptr();
|
||||||
|
hdr.sbp = sense.as_mut_ptr();
|
||||||
|
hdr.timeout = timeout_ms;
|
||||||
|
|
||||||
|
let ret = unsafe {
|
||||||
|
libc::ioctl(self.fd, SG_IO as _, &mut hdr as *mut sg_io_hdr)
|
||||||
|
};
|
||||||
|
|
||||||
|
if ret < 0 {
|
||||||
|
return Err(Error::IoError { source: std::io::Error::last_os_error() });
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes_transferred = (data.len() as i32 - hdr.resid) as usize;
|
||||||
|
|
||||||
|
if hdr.status != 0 {
|
||||||
|
let sense_key = if hdr.sb_len_wr > 2 { sense[2] & 0x0F } else { 0 };
|
||||||
|
return Err(Error::ScsiError {
|
||||||
|
opcode: cdb[0],
|
||||||
|
status: hdr.status,
|
||||||
|
sense_key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ScsiResult {
|
||||||
|
status: hdr.status,
|
||||||
|
bytes_transferred,
|
||||||
|
sense,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
//! SCSI/MMC command interface.
|
||||||
|
//!
|
||||||
|
//! Platform backends are in separate files:
|
||||||
|
//! - `linux.rs` — SG_IO ioctl
|
||||||
|
//! - `macos.rs` — IOKit (planned)
|
||||||
|
//! - `windows.rs` — SPTI (planned)
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux;
|
||||||
|
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
// ── SCSI opcodes (SPC-4, MMC-6) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub const SCSI_INQUIRY: u8 = 0x12;
|
||||||
|
pub const SCSI_READ_CAPACITY: u8 = 0x25;
|
||||||
|
pub const SCSI_READ_10: u8 = 0x28;
|
||||||
|
pub const SCSI_READ_BUFFER: u8 = 0x3C;
|
||||||
|
pub const SCSI_READ_TOC: u8 = 0x43;
|
||||||
|
pub const SCSI_GET_CONFIGURATION: u8 = 0x46;
|
||||||
|
pub const SCSI_SET_CD_SPEED: u8 = 0xBB;
|
||||||
|
pub const SCSI_SEND_KEY: u8 = 0xA3;
|
||||||
|
pub const SCSI_REPORT_KEY: u8 = 0xA4;
|
||||||
|
pub const SCSI_READ_12: u8 = 0xA8;
|
||||||
|
pub const SCSI_READ_DISC_STRUCTURE: u8 = 0xAD;
|
||||||
|
|
||||||
|
/// AACS key class for REPORT KEY / SEND KEY commands.
|
||||||
|
pub const AACS_KEY_CLASS: u8 = 0x02;
|
||||||
|
|
||||||
|
// ── Types ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum DataDirection {
|
||||||
|
None,
|
||||||
|
FromDevice,
|
||||||
|
ToDevice,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ScsiResult {
|
||||||
|
pub status: u8,
|
||||||
|
pub bytes_transferred: usize,
|
||||||
|
pub sense: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Low-level SCSI transport — one implementation per platform.
|
||||||
|
pub trait ScsiTransport {
|
||||||
|
fn execute(
|
||||||
|
&mut self,
|
||||||
|
cdb: &[u8],
|
||||||
|
direction: DataDirection,
|
||||||
|
data: &mut [u8],
|
||||||
|
timeout_ms: u32,
|
||||||
|
) -> Result<ScsiResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Platform-agnostic open ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Open a SCSI transport for the given device path.
|
||||||
|
/// Selects the right backend for the current platform.
|
||||||
|
pub fn open(device: &Path) -> Result<Box<dyn ScsiTransport>> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{ Ok(Box::new(linux::SgIoTransport::open(device)?)) }
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{ Err(Error::DeviceNotFound { path: format!("{}: macOS not yet supported", device.display()) }) }
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{ Err(Error::DeviceNotFound { path: format!("{}: Windows not yet supported", device.display()) }) }
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
|
||||||
|
{ Err(Error::DeviceNotFound { path: format!("{}: unsupported platform", device.display()) }) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CDB builders (platform-agnostic) ────────────────────────────────────────
|
||||||
|
|
||||||
|
/// SCSI INQUIRY response.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InquiryResult {
|
||||||
|
pub vendor_id: String,
|
||||||
|
pub model: String,
|
||||||
|
pub firmware: String,
|
||||||
|
pub raw: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send INQUIRY and parse standard response fields.
|
||||||
|
pub fn inquiry(scsi: &mut dyn ScsiTransport) -> Result<InquiryResult> {
|
||||||
|
let cdb = [SCSI_INQUIRY, 0x00, 0x00, 0x00, 0x60, 0x00];
|
||||||
|
let mut buf = [0u8; 96];
|
||||||
|
scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?;
|
||||||
|
|
||||||
|
Ok(InquiryResult {
|
||||||
|
vendor_id: String::from_utf8_lossy(&buf[8..16]).trim().to_string(),
|
||||||
|
model: String::from_utf8_lossy(&buf[16..32]).trim().to_string(),
|
||||||
|
firmware: String::from_utf8_lossy(&buf[32..36]).trim().to_string(),
|
||||||
|
raw: buf.to_vec(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send GET CONFIGURATION for feature 0x010C (Firmware Information).
|
||||||
|
pub fn get_config_010c(scsi: &mut dyn ScsiTransport) -> Result<Vec<u8>> {
|
||||||
|
let cdb = [SCSI_GET_CONFIGURATION, 0x02, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00];
|
||||||
|
let mut buf = [0u8; 16];
|
||||||
|
scsi.execute(&cdb, DataDirection::FromDevice, &mut buf, 5_000)?;
|
||||||
|
Ok(buf.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a READ BUFFER CDB.
|
||||||
|
pub fn build_read_buffer(mode: u8, buffer_id: u8, offset: u32, length: u32) -> [u8; 10] {
|
||||||
|
[
|
||||||
|
SCSI_READ_BUFFER, mode, buffer_id,
|
||||||
|
(offset >> 16) as u8, (offset >> 8) as u8, offset as u8,
|
||||||
|
(length >> 16) as u8, (length >> 8) as u8, length as u8,
|
||||||
|
0x00,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a SET CD SPEED CDB.
|
||||||
|
pub fn build_set_cd_speed(read_speed: u16) -> [u8; 12] {
|
||||||
|
[
|
||||||
|
SCSI_SET_CD_SPEED, 0x00,
|
||||||
|
(read_speed >> 8) as u8, read_speed as u8,
|
||||||
|
0xFF, 0xFF,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a READ(10) CDB with the raw read flag.
|
||||||
|
pub fn build_read10_raw(lba: u32, count: u16) -> [u8; 10] {
|
||||||
|
[
|
||||||
|
SCSI_READ_10, 0x08,
|
||||||
|
(lba >> 24) as u8, (lba >> 16) as u8, (lba >> 8) as u8, lba as u8,
|
||||||
|
0x00,
|
||||||
|
(count >> 8) as u8, count as u8,
|
||||||
|
0x00,
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user