This commit is contained in:
tux
2026-11-04 08:32:00 +01:00
parent 8012e912ec
commit 2d50b0fbea
16 changed files with 1542 additions and 215 deletions
+1
View File
@@ -325,6 +325,7 @@ impl Drive for ImageDrive {
TimedRead {
result,
milliseconds: ms,
microseconds: ms as i64 * 1000,
}
}
fn speed_set(&mut self, _speed: i32) -> i32 {
+128 -64
View File
@@ -179,6 +179,12 @@ pub struct LinuxDrive {
read_cmd: ReadCommand,
enable_mode: EnableMode,
last_milliseconds: i32,
last_microseconds: i64,
error_retry: bool,
/// Scratch buffer used by timing/analysis reads that do not request a
/// destination buffer. Reusing it avoids a multi-megabyte allocation on
/// every `read_timed(None, ...)` call, which would distort cache timing.
scratch: Vec<u8>,
}
impl LinuxDrive {
@@ -201,6 +207,9 @@ impl LinuxDrive {
read_cmd: ReadCommand::D8,
enable_mode: EnableMode::Dummy,
last_milliseconds: 0,
last_microseconds: -1,
error_retry: true,
scratch: Vec::new(),
};
d.inquiry()?;
d.check_atapi();
@@ -241,6 +250,9 @@ impl LinuxDrive {
read_cmd: ReadCommand::Read28,
enable_mode: EnableMode::Dummy,
last_milliseconds: 0,
last_microseconds: -1,
error_retry: true,
scratch: Vec::new(),
};
Ok(d)
}
@@ -566,7 +578,9 @@ impl LinuxDrive {
};
let started = Instant::now();
let rc = unsafe { libc::ioctl(self.fd, SG_IO, &mut hdr as *mut SgIoHdr) };
self.last_milliseconds = started.elapsed().as_millis() as i32;
let elapsed = started.elapsed();
self.last_microseconds = elapsed.as_micros() as i64;
self.last_milliseconds = ((elapsed.as_micros() + 500) / 1000) as i32;
if rc < 0 {
return Err(ScsiError::io(io::Error::last_os_error()));
}
@@ -703,10 +717,14 @@ impl LinuxDrive {
let _ = use_msf;
cdb[1] |= ((self.lun as u8) & 0x07) << 5;
let actual = self.sg_command(&cdb[..len], Some(&mut buf[..need]), SG_DXFER_FROM_DEV)?;
let count = count_2352_bytes(&buf[..need]);
let by_count = count / CD_FRAMESIZE_RAW;
let by_actual = actual / CD_FRAMESIZE_RAW;
Ok(by_count.min(by_actual))
// A successful ioctl with a completely untouched buffer means the
// transfer failed silently. Do not inspect individual trailing
// bytes: valid audio may legitimately end in any byte sequence,
// including the 0x7f fill pattern.
if actual >= need && buf[..need].iter().all(|b| *b == 0x7F) {
return Ok(0);
}
Ok((actual / CD_FRAMESIZE_RAW).min(sectors as usize))
}
fn scsi_read_map(
@@ -715,15 +733,28 @@ impl LinuxDrive {
p: Option<&mut [u8]>,
begin: i64,
sectors: i64,
) -> Result<usize, ReadError> {
let mut scratch = std::mem::take(&mut self.scratch);
let result = self.scsi_read_map_inner(command, p, begin, sectors, &mut scratch);
self.scratch = scratch;
result
}
fn scsi_read_map_inner(
&mut self,
command: ReadCommand,
p: Option<&mut [u8]>,
begin: i64,
sectors: i64,
scratch: &mut Vec<u8>,
) -> Result<usize, ReadError> {
let mut requested = sectors.min(self.nsectors).max(1);
let mut retry_count = 0;
let mut scratch;
let buffer: &mut [u8] = match p {
Some(b) => b,
None => {
scratch = vec![0u8; (requested * CD_FRAMESIZE_RAW as i64) as usize];
&mut scratch
scratch.resize((requested * CD_FRAMESIZE_RAW as i64) as usize, 0);
scratch.as_mut_slice()
}
};
if buffer.len() < (requested * CD_FRAMESIZE_RAW as i64) as usize {
@@ -748,6 +779,12 @@ impl LinuxDrive {
if errno == libc::ENOMEDIUM {
return Err(ReadError::no_medium());
}
if !self.error_retry {
return Err(ReadError::new(
errno,
format!("Read error at sector {}", begin),
));
}
if requested == 1 && retry_count > MAX_RETRIES - 1 {
return Err(ReadError::new(
errno,
@@ -801,15 +838,27 @@ impl LinuxDrive {
p: Option<&mut [u8]>,
begin: i64,
sectors: i64,
) -> Result<usize, ReadError> {
let mut scratch = std::mem::take(&mut self.scratch);
let result = self.cooked_read_inner(p, begin, sectors, &mut scratch);
self.scratch = scratch;
result
}
fn cooked_read_inner(
&mut self,
p: Option<&mut [u8]>,
begin: i64,
sectors: i64,
scratch: &mut Vec<u8>,
) -> Result<usize, ReadError> {
let mut requested = sectors.min(self.nsectors).max(1);
let mut retry = 0;
let mut scratch;
let buf: &mut [u8] = match p {
Some(b) => b,
None => {
scratch = vec![0u8; (requested * CD_FRAMESIZE_RAW as i64) as usize];
&mut scratch
scratch.resize((requested * CD_FRAMESIZE_RAW as i64) as usize, 0);
scratch.as_mut_slice()
}
};
if buf.len() < (requested * CD_FRAMESIZE_RAW as i64) as usize {
@@ -826,12 +875,20 @@ impl LinuxDrive {
};
let started = Instant::now();
let rc = unsafe { libc::ioctl(self.fd, CDROMREADAUDIO, &mut arg as *mut _) };
self.last_milliseconds = started.elapsed().as_millis() as i32;
let elapsed = started.elapsed();
self.last_microseconds = elapsed.as_micros() as i64;
self.last_milliseconds = ((elapsed.as_micros() + 500) / 1000) as i32;
if rc == 0 {
return Ok(requested as usize);
}
let err = io::Error::last_os_error();
let errno = err.raw_os_error().unwrap_or(libc::EIO);
if !self.error_retry {
return Err(ReadError::new(
errno,
format!("Read error at sector {}", begin),
));
}
if errno == libc::ENXIO || errno == libc::EBADF || errno == libc::ENOMEDIUM {
return Err(ReadError::no_medium());
}
@@ -859,40 +916,33 @@ impl LinuxDrive {
fn verify_read_command(&mut self) -> Result<(), String> {
let mut buf = vec![0u8; CD_FRAMESIZE_RAW];
let mut tried_any = false;
let tried = [self.read_cmd];
for command in tried {
if self.try_read_command(command, &mut buf) {
if self.try_read_command(self.read_cmd, &mut buf, false) {
return Ok(());
}
let all = [
ReadCommand::Read28,
ReadCommand::ReadA8,
ReadCommand::MmcB10,
ReadCommand::MmcB2_10,
ReadCommand::MmcB6_F8,
ReadCommand::MmcB2_F8,
ReadCommand::MmcB4_F8,
ReadCommand::Mmc2_F8,
ReadCommand::Mmc3_F8,
ReadCommand::Msf10,
ReadCommand::MsfF8,
ReadCommand::Msf3F8,
ReadCommand::D4_10,
ReadCommand::D4_12,
ReadCommand::D5,
ReadCommand::D8,
];
for command in all {
if self.try_read_command(command, &mut buf, true) {
self.read_cmd = command;
return Ok(());
}
tried_any = true;
}
if tried_any {
let all = [
ReadCommand::Read28,
ReadCommand::ReadA8,
ReadCommand::MmcB10,
ReadCommand::MmcB2_10,
ReadCommand::MmcB6_F8,
ReadCommand::MmcB2_F8,
ReadCommand::MmcB4_F8,
ReadCommand::Mmc2_F8,
ReadCommand::Mmc3_F8,
ReadCommand::Msf10,
ReadCommand::MsfF8,
ReadCommand::Msf3F8,
ReadCommand::D4_10,
ReadCommand::D4_12,
ReadCommand::D5,
ReadCommand::D8,
];
for command in all {
if self.try_read_command(command, &mut buf) {
self.read_cmd = command;
return Ok(());
}
}
}
Err(
"Unable to find any suitable command set from probe; drive probably not CDDA capable"
@@ -900,7 +950,12 @@ impl LinuxDrive {
)
}
fn try_read_command(&mut self, command: ReadCommand, buf: &mut [u8]) -> bool {
fn try_read_command(
&mut self,
command: ReadCommand,
buf: &mut [u8],
require_nonzero: bool,
) -> bool {
if self.enable_cdda().is_err() {
return false;
}
@@ -910,10 +965,7 @@ impl LinuxDrive {
let last = self.toc.track_lastsector(t);
let sector = (first + last) >> 1;
if let Ok(n) = self.read_once(command, sector, 1, buf) {
if n > 0
&& count_2352_bytes(buf) == CD_FRAMESIZE_RAW
&& buf.iter().any(|b| *b != 0)
{
if n > 0 && (!require_nonzero || buf.iter().any(|b| *b != 0)) {
return true;
}
}
@@ -1073,15 +1125,6 @@ fn fft(re: &mut [f64], im: &mut [f64]) {
}
}
fn count_2352_bytes(buf: &[u8]) -> usize {
for i in (0..buf.len()).rev() {
if buf[i] != 0x7F {
return ((i + 3) >> 2) << 2;
}
}
0
}
fn check_sbp(status: u8, sense: &[u8]) -> ScsiError {
let key = sense.get(2).copied().unwrap_or(0) & 0x0F;
let asc = sense.get(12).copied().unwrap_or(0);
@@ -1219,10 +1262,12 @@ impl Drive for LinuxDrive {
}
fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead {
self.last_milliseconds = -1;
self.last_microseconds = -1;
let result = self.read_audio(buffer, begin, sectors);
TimedRead {
result,
milliseconds: self.last_milliseconds,
microseconds: self.last_microseconds,
}
}
fn speed_set(&mut self, speed: i32) -> i32 {
@@ -1268,9 +1313,30 @@ impl Drive for LinuxDrive {
fn set_nsectors(&mut self, value: i64) {
self.nsectors = value;
}
fn set_error_retry(&mut self, enabled: bool) {
self.error_retry = enabled;
}
fn interface(&self) -> InterfaceKind {
self.interface
}
fn is_atapi(&self) -> bool {
self.is_atapi
}
fn is_mmc(&self) -> bool {
self.is_mmc
}
fn density(&self) -> u8 {
self.density
}
fn fua(&self) -> bool {
self.fua
}
fn lun(&self) -> i32 {
self.lun
}
fn adjust_ssize(&self) -> i64 {
self.adjust_ssize
}
}
impl Drop for LinuxDrive {
@@ -1295,15 +1361,13 @@ mod tests {
}
#[test]
fn fill_byte_counting() {
fn fill_pattern_detection() {
let mut buf = vec![0x7Fu8; 2352 * 2];
assert_eq!(count_2352_bytes(&buf), 0);
assert!(buf.iter().all(|b| *b == 0x7F));
buf[10] = 0;
assert_eq!(count_2352_bytes(&buf), 12);
for b in buf.iter_mut() {
*b = 0;
}
assert_eq!(count_2352_bytes(&buf), buf.len());
assert!(buf.iter().any(|b| *b != 0x7F));
let tail = [0x7Fu8; 16];
assert!(tail.iter().all(|b| *b == 0x7F));
}
#[test]
+26
View File
@@ -252,7 +252,11 @@ impl fmt::Display for ReadError {
#[derive(Debug, Clone)]
pub struct TimedRead {
pub result: Result<usize, ReadError>,
/// Whole milliseconds, matching the C ABI.
pub milliseconds: i32,
/// Higher precision duration for internal timing analysis. Backends
/// that only know millisecond figures fill this with `ms * 1000`.
pub microseconds: i64,
}
/// Backend interface used by both the command line program and the C ABI.
@@ -318,7 +322,29 @@ pub trait Drive {
fn set_bigendianp(&mut self, value: i32);
fn nsectors(&self) -> i64;
fn set_nsectors(&mut self, value: i64);
/// Enable or disable the low-level read retry loop. Cache analysis
/// disables retries so that timing reflects the first access.
fn set_error_retry(&mut self, _enabled: bool) {}
fn interface(&self) -> InterfaceKind;
/// Optional drive attributes exposed through the C ABI.
fn is_atapi(&self) -> bool {
false
}
fn is_mmc(&self) -> bool {
false
}
fn density(&self) -> u8 {
0
}
fn fua(&self) -> bool {
false
}
fn lun(&self) -> i32 {
0
}
fn adjust_ssize(&self) -> i64 {
1
}
fn cdda_version(&self) -> &'static str {
crate::VERSIONNUM
}
+14 -2
View File
@@ -15,8 +15,8 @@ pub mod callback;
pub mod device;
pub mod paranoia;
pub const VERSIONNUM: &str = "26.0.1";
pub const VERSION: &str = "cdrustanoia 26.0.1 (2026-11-03)";
pub const VERSIONNUM: &str = env!("CARGO_PKG_VERSION");
pub const VERSION: &str = concat!("cdrustanoia ", env!("CARGO_PKG_VERSION"));
pub fn paranoia_version() -> &'static str {
VERSIONNUM
@@ -25,3 +25,15 @@ pub fn paranoia_version() -> &'static str {
pub fn cdda_version() -> &'static str {
VERSIONNUM
}
#[cfg(test)]
mod tests {
#[test]
fn versions_come_from_cargo() {
assert_eq!(super::VERSIONNUM, env!("CARGO_PKG_VERSION"));
assert_eq!(
super::VERSION,
concat!("cdrustanoia ", env!("CARGO_PKG_VERSION"))
);
}
}
+35
View File
@@ -128,3 +128,38 @@ fn jittered_image_roundtrip() {
};
run_config(sectors, config);
}
#[test]
fn fragmented_image_roundtrip() {
use cdrustanoia_core::device::image::{ImageConfig, ImageFaults};
let sectors = 24usize;
let config = ImageConfig {
tracks: vec![(0, sectors as i64 - 1, 0)],
faults: ImageFaults {
max_read_sectors: 3,
..Default::default()
},
..Default::default()
};
run_config(sectors, config);
}
#[test]
fn silent_image_roundtrip() {
use cdrustanoia_core::device::image::{ImageConfig, ImageFaults};
let sectors = 24usize;
let config = ImageConfig {
tracks: vec![(0, sectors as i64 - 1, 0)],
faults: ImageFaults::default(),
..Default::default()
};
let bytes = vec![0u8; sectors * CD_FRAMESIZE_RAW];
let drive = ImageDrive::with_config(bytes, config);
let mut p = Paranoia::new(Box::new(drive));
p.modeset(PARANOIA_MODE_FULL);
let mut cb = |_: i64, _: i32| {};
for _ in 0..sectors {
let sec = p.read(&mut cb).expect("read silent sector");
assert!(sec.iter().all(|s| *s == 0));
}
}