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
+3
View File
@@ -14,3 +14,6 @@ path = "src/lib.rs"
cdrustanoia_core.workspace = true
cdrustanoia_capi_types.workspace = true
libc.workspace = true
[dev-dependencies]
tempfile = "3"
+137 -5
View File
@@ -8,9 +8,11 @@
use cdrustanoia_capi_types::*;
use cdrustanoia_core::device::discover;
use cdrustanoia_core::device::image::ImageDrive;
use cdrustanoia_core::device::{DiscToc, Drive, InterfaceKind, ReadError};
use libc::{c_char, c_int, c_long, c_void};
use std::ffi::CString;
use std::path::Path;
use std::ptr;
pub struct BackendHolder {
@@ -42,6 +44,12 @@ unsafe fn set_error(d: *mut CdromDrive, message: &str) {
(*d).errorbuf = cstr(message);
}
unsafe fn set_message(messages: *mut *mut c_char, message: &str) {
if !messages.is_null() && (*messages).is_null() {
*messages = cstr(message);
}
}
pub unsafe fn holder<'a>(d: *mut CdromDrive) -> Option<&'a mut BackendHolder> {
if d.is_null() {
return None;
@@ -65,6 +73,13 @@ pub unsafe fn make_drive(
) -> *mut CdromDrive {
let mut d = Box::new(CdromDrive::empty());
d.drive_type = 0;
d.is_atapi = backend.is_atapi() as c_int;
d.is_mmc = backend.is_mmc() as c_int;
d.density = backend.density();
d.fua = backend.fua() as c_int;
d.lun = backend.lun();
d.adjust_ssize = backend.adjust_ssize() as c_int;
d.bigbuff = backend.nsectors().max(0).saturating_mul(2352) as c_long;
d.interface = match backend.interface() {
InterfaceKind::SgIo => INTERFACE_SGIO_SCSI,
InterfaceKind::Cooked => INTERFACE_COOKED_IOCTL,
@@ -141,10 +156,23 @@ unsafe fn read_into(
sectors: c_long,
) -> Result<usize, ReadError> {
let h = holder(d).ok_or_else(ReadError::closed)?;
if !d.is_null() {
h.drive.set_error_retry((*d).error_retry != 0);
if (*d).nsectors > 0 {
h.drive.set_nsectors((*d).nsectors as i64);
}
if (*d).bigendianp >= 0 {
h.drive.set_bigendianp((*d).bigendianp);
}
}
if p.is_null() {
h.drive.read_audio_raw(None, begin as i64, sectors as i64)
} else {
let len = (sectors.max(0) as usize) * cdrustanoia_core::device::CD_FRAMESIZE_RAW;
let Some(len) =
(sectors.max(0) as usize).checked_mul(cdrustanoia_core::device::CD_FRAMESIZE_RAW)
else {
return Err(ReadError::new(libc::EINVAL, "Invalid read size"));
};
let buf = std::slice::from_raw_parts_mut(p as *mut u8, len);
h.drive
.read_audio_raw(Some(buf), begin as i64, sectors as i64)
@@ -212,6 +240,7 @@ unsafe fn fill_toc(d: *mut CdromDrive) {
(*d).audio_last_sector = toc.disc_lastsector() as c_long;
(*d).bigendianp = h.drive.bigendianp();
(*d).nsectors = h.drive.nsectors() as c_int;
(*d).bigbuff = (*d).nsectors.max(0) as c_long * 2352;
}
#[no_mangle]
@@ -242,7 +271,13 @@ unsafe fn cdda_identify_impl(
};
match discover::identify_device(device, messagedest == CDDA_MESSAGE_PRINTIT) {
Ok(drive) => make_drive(device, Some(device), drive),
Err(_) => ptr::null_mut(),
Err(e) => {
set_message(
messages,
&format!("\tCould not identify {}: {}\n", device, e),
);
ptr::null_mut()
}
}
}
@@ -274,7 +309,13 @@ unsafe fn cdda_identify_cooked_impl(
};
match discover::identify_cooked(device, messagedest == CDDA_MESSAGE_PRINTIT) {
Ok(drive) => make_drive(device, Some(device), drive),
Err(_) => ptr::null_mut(),
Err(e) => {
set_message(
messages,
&format!("\tCould not identify cooked device {}: {}\n", device, e),
);
ptr::null_mut()
}
}
}
@@ -320,9 +361,64 @@ unsafe fn cdda_identify_scsi_impl(
return make_drive(dev, Some(dev), drive);
}
}
set_message(
messages,
"\tCould not identify a usable SCSI/cooked device\n",
);
ptr::null_mut()
}
#[no_mangle]
pub unsafe extern "C" fn cdda_identify_test(
filename: *const c_char,
messagedest: c_int,
messages: *mut *mut c_char,
) -> *mut CdromDrive {
ffi_guard(ptr::null_mut(), || {
cdda_identify_test_impl(filename, messagedest, messages)
})
}
unsafe fn cdda_identify_test_impl(
filename: *const c_char,
messagedest: c_int,
messages: *mut *mut c_char,
) -> *mut CdromDrive {
if !messages.is_null() {
*messages = ptr::null_mut();
}
if filename.is_null() {
set_message(messages, "\tNo filename supplied for test interface\n");
return ptr::null_mut();
}
let filename = match std::ffi::CStr::from_ptr(filename).to_str() {
Ok(value) => value,
Err(_) => return ptr::null_mut(),
};
let verbose = messagedest == CDDA_MESSAGE_PRINTIT;
if verbose {
eprintln!("\tTesting {} for file/test interface", filename);
}
match ImageDrive::open_path(Path::new(filename)) {
Ok(drive) => {
if verbose {
eprintln!("\t\tCDROM sensed: {}", drive.drive_model());
}
make_drive(filename, None, Box::new(drive))
}
Err(e) => {
if verbose {
eprintln!("\t\tCould not open file {}: {}", filename, e);
}
set_message(
messages,
&format!("\tCould not open file {}: {}\n", filename, e),
);
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn cdda_find_a_cdrom(
messagedest: c_int,
@@ -342,7 +438,10 @@ unsafe fn cdda_find_a_cdrom_impl(
}
match discover::find_a_cdrom(messagedest == CDDA_MESSAGE_PRINTIT) {
Some(drive) => make_drive("auto", None, drive),
None => ptr::null_mut(),
None => {
set_message(messages, "\tNo CD-ROM drive found\n");
ptr::null_mut()
}
}
}
@@ -443,6 +542,7 @@ unsafe fn cdda_read_timed_impl(
return -400;
}
};
h.drive.set_error_retry((*d).error_retry != 0);
if (*d).bigendianp >= 0 {
h.drive.set_bigendianp((*d).bigendianp);
}
@@ -452,7 +552,12 @@ unsafe fn cdda_read_timed_impl(
let timed = if buffer.is_null() {
h.drive.read_timed(None, beginsector as i64, sectors as i64)
} else {
let len = (sectors as usize) * cdrustanoia_core::device::CD_FRAMESIZE_RAW;
let Some(len) = (sectors as usize).checked_mul(cdrustanoia_core::device::CD_FRAMESIZE_RAW)
else {
set_errno(libc::EINVAL);
set_error(d, "Invalid read size\n");
return -7;
};
let buf = std::slice::from_raw_parts_mut(buffer as *mut u8, len);
h.drive
.read_timed(Some(buf), beginsector as i64, sectors as i64)
@@ -460,6 +565,9 @@ unsafe fn cdda_read_timed_impl(
if !ms.is_null() {
*ms = timed.milliseconds;
}
if !(*d).private.is_null() {
(*(*d).private).last_milliseconds = timed.milliseconds;
}
if (*d).bigendianp < 0 {
(*d).bigendianp = h.drive.bigendianp();
}
@@ -680,6 +788,7 @@ mod tests {
TimedRead {
result: Err(ReadError::closed()),
milliseconds: -1,
microseconds: -1,
}
}
fn speed_set(&mut self, _speed: i32) -> i32 {
@@ -702,6 +811,29 @@ mod tests {
}
}
#[test]
fn c_abi_version_stays_cdparanoia_compatible() {
let version = unsafe { std::ffi::CStr::from_ptr(cdda_version()) };
assert_eq!(version.to_str().unwrap(), "10.2");
}
#[test]
fn test_interface_exposes_image_backend() {
let dir = tempfile::tempdir().unwrap();
let image = dir.path().join("disc.raw");
std::fs::write(&image, vec![0u8; 2352 * 10]).unwrap();
let name = CString::new(image.to_string_lossy().as_bytes()).unwrap();
unsafe {
let d = cdda_identify_test(name.as_ptr(), CDDA_MESSAGE_FORGETIT, ptr::null_mut());
assert!(!d.is_null());
assert_eq!((*d).interface, INTERFACE_TEST);
assert_eq!(cdda_open(d), 0);
assert_eq!(cdda_tracks(d), 1);
assert_eq!(cdda_disc_lastsector(d), 9);
assert_eq!(cdda_close(d), 0);
}
}
#[test]
fn c_api_catches_panics() {
unsafe {
+16 -1
View File
@@ -133,9 +133,11 @@ impl Drive for CAbiDrive {
fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead {
let started = Instant::now();
let result = self.read_audio(buffer, begin, sectors);
let elapsed = started.elapsed();
TimedRead {
result,
milliseconds: started.elapsed().as_millis() as i32,
milliseconds: ((elapsed.as_micros() + 500) / 1000) as i32,
microseconds: elapsed.as_micros() as i64,
}
}
fn speed_set(&mut self, speed: i32) -> i32 {
@@ -177,6 +179,13 @@ impl Drive for CAbiDrive {
}
}
}
fn set_error_retry(&mut self, enabled: bool) {
unsafe {
if !self.d.is_null() {
(*self.d).error_retry = enabled as c_int;
}
}
}
fn interface(&self) -> InterfaceKind {
self.interface
}
@@ -302,6 +311,12 @@ mod tests {
use cdrustanoia_core::device::samples_to_bytes;
use cdrustanoia_core::device::CD_FRAMEWORDS;
#[test]
fn c_abi_version_stays_cdparanoia_compatible() {
let version = unsafe { std::ffi::CStr::from_ptr(paranoia_version()) };
assert_eq!(version.to_str().unwrap(), "10.2");
}
#[test]
fn c_api_query_and_read() {
unsafe {
+1067 -135
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -22,8 +22,7 @@ use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::FromRawFd;
use std::path::Path;
const MANUAL: &str = r#"cdrustanoia 26.0.1 (2026-11-03)
based on the great cdparanoia by Monty <monty@xiph.org> and Xiph.Org
const MANUAL_BODY: &str = r#"based on the great cdparanoia by Monty <monty@xiph.org> and Xiph.Org
(C) 2026 Martin Wohlert <martin.wohlert@b-root-force.de>
@@ -167,7 +166,8 @@ A few examples, protected from the shell:
"#;
fn usage(out: &mut dyn Write) {
let _ = out.write_all(MANUAL.as_bytes());
let _ = writeln!(out, "{}", VERSION);
let _ = out.write_all(MANUAL_BODY.as_bytes());
}
fn open_log_name(name: &str) -> Result<Box<dyn Write>, String> {
@@ -358,7 +358,7 @@ pub fn run(argv: &[String]) -> i32 {
return 0;
}
if args.version {
eprintln!("{}\n", VERSION);
eprintln!("{}", VERSION);
return 0;
}
+99
View File
@@ -23,6 +23,79 @@ fn end_to_end_image_rip() {
assert_eq!(cdrustanoia_cli::run(&argv), 0);
let got = std::fs::read(&out).unwrap();
assert_eq!(got, samples_to_bytes(&samples));
// Output formats and sample offsets use the same source data.
let raw_bytes = samples_to_bytes(&samples);
let wav = dir.path().join("format.wav");
let argv = vec![
"cdparanoia".to_string(),
"-q".to_string(),
"-w".to_string(),
"1".to_string(),
wav.to_string_lossy().into_owned(),
];
assert_eq!(cdrustanoia_cli::run(&argv), 0);
let wav_bytes = std::fs::read(&wav).unwrap();
assert_eq!(&wav_bytes[..4], b"RIFF");
assert_eq!(&wav_bytes[44..], raw_bytes.as_slice());
let aiff = dir.path().join("format.aiff");
let argv = vec![
"cdparanoia".to_string(),
"-q".to_string(),
"-f".to_string(),
"1".to_string(),
aiff.to_string_lossy().into_owned(),
];
assert_eq!(cdrustanoia_cli::run(&argv), 0);
let aiff_bytes = std::fs::read(&aiff).unwrap();
assert_eq!(&aiff_bytes[..4], b"FORM");
let mut big_endian = Vec::with_capacity(samples.len() * 2);
for sample in &samples {
big_endian.extend_from_slice(&sample.to_be_bytes());
}
assert_eq!(&aiff_bytes[54..], big_endian.as_slice());
let offset_out = dir.path().join("offset.raw");
let argv = vec![
"cdparanoia".to_string(),
"-q".to_string(),
"-r".to_string(),
"-O".to_string(),
"10".to_string(),
"1".to_string(),
offset_out.to_string_lossy().into_owned(),
];
assert_eq!(cdrustanoia_cli::run(&argv), 0);
let offset_bytes = std::fs::read(&offset_out).unwrap();
assert_eq!(&offset_bytes[..raw_bytes.len() - 40], &raw_bytes[40..]);
for mode in ["-Z", "-Y"] {
let out = dir
.path()
.join(format!("mode{}.raw", mode.trim_start_matches('-')));
let argv = vec![
"cdparanoia".to_string(),
"-q".to_string(),
"-r".to_string(),
mode.to_string(),
"1".to_string(),
out.to_string_lossy().into_owned(),
];
assert_eq!(cdrustanoia_cli::run(&argv), 0);
assert_eq!(std::fs::read(&out).unwrap(), raw_bytes);
}
// The compact image has no simulated cache; -A must complete cleanly.
let analyze_log = dir.path().join("analyze.log");
let analyze_argv = vec![
"cdparanoia".to_string(),
"-A".to_string(),
format!("-L{}", analyze_log.to_string_lossy()),
];
std::env::set_var("CDRUSTANOIA_TEST_IMAGE", &image);
assert_eq!(cdrustanoia_cli::run(&analyze_argv), 0);
let analyze_text = std::fs::read_to_string(&analyze_log).unwrap();
assert!(analyze_text.contains("Checking drive cache/timing behavior"));
std::env::remove_var("CDRUSTANOIA_TEST_IMAGE");
// Multi-track batch mode.
@@ -48,3 +121,29 @@ fn end_to_end_image_rip() {
assert_eq!(&t2[44..], &samples_to_bytes(&multi_samples[24 * 1176..]));
std::env::remove_var("CDRUSTANOIA_TEST_IMAGE");
}
#[test]
fn version_output_matches_cdparanoia_shape() {
let exe = env!("CARGO_BIN_EXE_cdparanoia");
let out = std::process::Command::new(exe)
.arg("-V")
.output()
.expect("run cdparanoia -V");
assert!(out.status.success());
let expected = format!("{}\n", cdrustanoia_core::VERSION);
assert_eq!(String::from_utf8_lossy(&out.stderr), expected);
}
#[test]
fn help_starts_with_cargo_version() {
let exe = env!("CARGO_BIN_EXE_cdparanoia");
let out = std::process::Command::new(exe)
.arg("--help")
.output()
.expect("run cdparanoia --help");
assert!(out.status.success());
let text = String::from_utf8_lossy(&out.stdout);
let first = text.lines().next().unwrap_or_default();
assert_eq!(first, cdrustanoia_core::VERSION);
assert!(!text.contains("2026-11-03"));
}
+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));
}
}