use libc::{O_NONBLOCK, O_RDWR, c_uchar, c_void};
use std::cmp::min;
use std::ffi::CString;
use std::fs::File;
use std::io::Error;
use std::os::fd::{AsRawFd, FromRawFd};
use std::path::Path;
use std::thread::sleep;
use std::time::Duration;
use crate::Toc;
use crate::parse_toc::parse_toc;
use crate::utils::get_track_bounds;
use crate::{CdReaderError, RetryConfig, ScsiError, ScsiOp};
const SG_INFO_CHECK: u32 = 0x1;
const SG_DXFER_FROM_DEV: i32 = -3;
#[repr(C)]
struct SgIoHeader {
interface_id: i32, dxfer_direction: i32, cmd_len: u8, mx_sb_len: u8, iovec_count: u16,
dxfer_len: u32, dxferp: *mut c_void, cmdp: *mut c_uchar, sbp: *mut c_uchar, timeout: u32, flags: u32,
pack_id: i32,
usr_ptr: *mut 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,
}
const SG_IO: u64 = 0x2285;
static mut DRIVE_HANDLE: Option<File> = None;
pub fn list_drive_paths() -> std::io::Result<Vec<String>> {
let mut drives = Vec::new();
if let Ok(entries) = std::fs::read_dir("/sys/class/block") {
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with("sr") {
drives.push(format!("/dev/{name}"));
}
}
}
if drives.is_empty() && Path::new("/dev/cdrom").exists() {
drives.push("/dev/cdrom".to_string());
}
drives.sort();
drives.dedup();
Ok(drives)
}
pub fn open_drive(path: &str) -> std::io::Result<()> {
let c = CString::new(path).unwrap();
let fd = unsafe { libc::open(c.as_ptr(), O_RDWR | O_NONBLOCK) };
if fd < 0 {
return Err(Error::last_os_error());
}
let drive_handle = unsafe { File::from_raw_fd(fd) };
unsafe {
DRIVE_HANDLE = Some(drive_handle);
}
Ok(())
}
#[allow(static_mut_refs)]
pub fn close_drive() {
unsafe {
if let Some(current_drive) = DRIVE_HANDLE.take() {
drop(current_drive);
DRIVE_HANDLE = None;
}
}
}
#[allow(static_mut_refs)]
unsafe fn ioctl_sg_io(hdr: &mut SgIoHeader) -> std::io::Result<()> {
let fd = unsafe {
DRIVE_HANDLE
.as_ref()
.ok_or_else(|| Error::new(std::io::ErrorKind::NotFound, "Drive not opened"))?
.as_raw_fd()
};
let ret = unsafe { libc::ioctl(fd, SG_IO, hdr as *mut _) };
if ret < 0 {
return Err(Error::last_os_error());
}
Ok(())
}
pub fn read_toc() -> std::result::Result<Toc, CdReaderError> {
let alloc_len: usize = 2048;
let mut data = vec![0u8; alloc_len];
let mut sense = vec![0u8; 32];
let mut cdb = [0u8; 10];
cdb[0] = 0x43;
cdb[1] = 0; cdb[2] = 0; cdb[6] = 0; cdb[7] = ((alloc_len >> 8) & 0xFF) as u8;
cdb[8] = (alloc_len & 0xFF) as u8;
let mut hdr = SgIoHeader {
interface_id: 'S' as i32,
dxfer_direction: SG_DXFER_FROM_DEV,
cmd_len: cdb.len() as u8,
mx_sb_len: sense.len() as u8,
iovec_count: 0,
dxfer_len: data.len() as u32,
dxferp: data.as_mut_ptr() as *mut c_void,
cmdp: cdb.as_mut_ptr(),
sbp: sense.as_mut_ptr(),
timeout: 10_000, flags: 0,
pack_id: 0,
usr_ptr: std::ptr::null_mut(),
status: 0,
masked_status: 0,
msg_status: 0,
sb_len_wr: 0,
host_status: 0,
driver_status: 0,
resid: 0,
duration: 0,
info: 0,
};
unsafe { ioctl_sg_io(&mut hdr).map_err(CdReaderError::Io)? };
if hdr.info & SG_INFO_CHECK != 0 {
let (sense_key, asc, ascq) = parse_sense(&sense, hdr.sb_len_wr);
return Err(CdReaderError::Scsi(ScsiError {
op: ScsiOp::ReadToc,
lba: None,
sectors: None,
scsi_status: hdr.status,
sense_key,
asc,
ascq,
}));
}
if hdr.status != 0 {
let (sense_key, asc, ascq) = parse_sense(&sense, hdr.sb_len_wr);
return Err(CdReaderError::Scsi(ScsiError {
op: ScsiOp::ReadToc,
lba: None,
sectors: None,
scsi_status: hdr.status,
sense_key,
asc,
ascq,
}));
}
if hdr.resid > 0 {
let got = data.len() as i32 - hdr.resid;
if got > 0 {
data.truncate(got as usize);
}
}
parse_toc(data).map_err(|err| CdReaderError::Parse(err.to_string()))
}
pub fn read_track_with_retry(
toc: &Toc,
track_no: u8,
cfg: &RetryConfig,
) -> std::result::Result<Vec<u8>, CdReaderError> {
let (start_lba, sectors) = get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?;
read_sectors_with_retry(start_lba, sectors, cfg)
}
pub fn read_sectors_with_retry(
start_lba: u32,
sectors: u32,
cfg: &RetryConfig,
) -> std::result::Result<Vec<u8>, CdReaderError> {
read_cd_audio_range(start_lba, sectors, cfg)
}
fn read_cd_audio_range(
start_lba: u32,
sectors: u32,
cfg: &RetryConfig,
) -> std::result::Result<Vec<u8>, CdReaderError> {
const SECTOR_BYTES: usize = 2352;
const MAX_SECTORS_PER_XFER: u32 = 27;
let total_bytes = (sectors as usize) * SECTOR_BYTES;
let mut out = Vec::<u8>::with_capacity(total_bytes);
let mut remaining = sectors;
let mut lba = start_lba;
let attempts_total = cfg.max_attempts.max(1);
while remaining > 0 {
let mut chunk_sectors = min(remaining, MAX_SECTORS_PER_XFER);
let min_chunk = cfg.min_sectors_per_read.max(1);
let mut backoff_ms = cfg.initial_backoff_ms;
let mut last_err: Option<CdReaderError> = None;
for attempt in 1..=attempts_total {
match read_cd_audio_chunk(lba, chunk_sectors) {
Ok(chunk) => {
out.extend_from_slice(&chunk);
lba += chunk_sectors;
remaining -= chunk_sectors;
last_err = None;
break;
}
Err(err) => {
last_err = Some(err);
if attempt == attempts_total {
break;
}
if cfg.reduce_chunk_on_retry && chunk_sectors > min_chunk {
chunk_sectors = next_chunk_size(chunk_sectors, min_chunk);
}
if backoff_ms > 0 {
sleep(Duration::from_millis(backoff_ms));
}
if cfg.max_backoff_ms > 0 {
backoff_ms = (backoff_ms.saturating_mul(2)).min(cfg.max_backoff_ms);
}
}
}
}
if let Some(err) = last_err {
return Err(err);
}
}
Ok(out)
}
fn read_cd_audio_chunk(lba: u32, this_sectors: u32) -> std::result::Result<Vec<u8>, CdReaderError> {
const SECTOR_BYTES: usize = 2352;
let mut chunk = vec![0u8; (this_sectors as usize) * SECTOR_BYTES];
let mut sense = vec![0u8; 64];
let mut cdb = [0u8; 12];
cdb.fill(0);
cdb[0] = 0xBE; cdb[2] = ((lba >> 24) & 0xFF) as u8;
cdb[3] = ((lba >> 16) & 0xFF) as u8;
cdb[4] = ((lba >> 8) & 0xFF) as u8;
cdb[5] = (lba & 0xFF) as u8;
cdb[6] = ((this_sectors >> 16) & 0xFF) as u8;
cdb[7] = ((this_sectors >> 8) & 0xFF) as u8;
cdb[8] = (this_sectors & 0xFF) as u8;
cdb[9] = 0x10;
cdb[10] = 0x00;
cdb[11] = 0x00;
let mut hdr = SgIoHeader {
interface_id: 'S' as i32,
dxfer_direction: SG_DXFER_FROM_DEV,
cmd_len: cdb.len() as u8,
mx_sb_len: sense.len() as u8,
iovec_count: 0,
dxfer_len: chunk.len() as u32,
dxferp: chunk.as_mut_ptr() as *mut c_void,
cmdp: cdb.as_mut_ptr(),
sbp: sense.as_mut_ptr(),
timeout: 30_000, flags: 0,
pack_id: 0,
usr_ptr: std::ptr::null_mut(),
status: 0,
masked_status: 0,
msg_status: 0,
sb_len_wr: 0,
host_status: 0,
driver_status: 0,
resid: 0,
duration: 0,
info: 0,
};
unsafe { ioctl_sg_io(&mut hdr).map_err(CdReaderError::Io)? };
if hdr.info & SG_INFO_CHECK != 0 || hdr.status != 0 {
let (sense_key, asc, ascq) = parse_sense(&sense, hdr.sb_len_wr);
return Err(CdReaderError::Scsi(ScsiError {
op: ScsiOp::ReadCd,
lba: Some(lba),
sectors: Some(this_sectors),
scsi_status: hdr.status,
sense_key,
asc,
ascq,
}));
}
if hdr.resid > 0 {
let got = (chunk.len() as i32 - hdr.resid).max(0) as usize;
chunk.truncate(got);
}
Ok(chunk)
}
fn parse_sense(sense: &[u8], sb_len_wr: u8) -> (Option<u8>, Option<u8>, Option<u8>) {
if sb_len_wr == 0 || sense.len() < 14 {
return (None, None, None);
}
let sense_key = Some(sense[2] & 0x0F);
let asc = Some(sense[12]);
let ascq = Some(sense[13]);
(sense_key, asc, ascq)
}
fn next_chunk_size(current: u32, min_chunk: u32) -> u32 {
if current > 8 {
8.max(min_chunk)
} else {
min_chunk
}
}