updates
This commit is contained in:
Generated
+1
@@ -15,6 +15,7 @@ dependencies = [
|
||||
"cdrustanoia_capi_types",
|
||||
"cdrustanoia_core",
|
||||
"libc",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "26.0.1"
|
||||
version = "26.0.2"
|
||||
edition = "2021"
|
||||
license = "LGPL-2.1-or-later"
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ check:
|
||||
$(CARGO) fmt --all -- --check
|
||||
$(CARGO) clippy --workspace --all-targets -- -D warnings
|
||||
$(CARGO) test --workspace
|
||||
$(CC) -std=gnu11 -Wall -Wextra -Werror -Wno-unused-variable -Wno-unused-function -Iinclude -c tests/capi_compile.c -o target/capi_compile.o
|
||||
$(CC) -std=gnu11 -Wall -Wextra -Werror -Wno-unused-variable -Wno-unused-function -DCDDA_TEST -Iinclude -c tests/capi_compile.c -o target/capi_compile.o
|
||||
|
||||
install: build
|
||||
$(INSTALL) -d -m 0755 $(BINDIR)
|
||||
|
||||
@@ -84,6 +84,8 @@ cc app.c -lcdda_paranoia -lcdda_interface
|
||||
|
||||
The public `cdrom_drive` layout, `TOC` layout, function-pointer fields, error
|
||||
codes and `paranoia_*` entry points match cdparanoia 10.2 on 64-bit Linux.
|
||||
The test interface is exported as `cdda_identify_test()` and declared when a
|
||||
caller defines `CDDA_TEST`, matching the original source tree.
|
||||
|
||||
## Testing without a CD drive
|
||||
|
||||
@@ -144,8 +146,10 @@ CD-ROM drive. When hardware is available, verify:
|
||||
generic device where the kernel supports it.
|
||||
- `-i/--output-info` is accepted but is a no-op, matching the 10.2 source.
|
||||
- 64-bit Linux ABI is the primary target.
|
||||
- The `-A` cache analysis is a compact functional port; its timing
|
||||
recommendations should be treated as advisory.
|
||||
- `-A` is a full port of `cachetest.c`, including the fast/slow cache-size
|
||||
search, readahead, rollbehind, cache-tail granularity and backseek-flush
|
||||
tests. Its findings still depend on drive firmware and system load, as with
|
||||
the original.
|
||||
|
||||
## Licence
|
||||
|
||||
|
||||
@@ -14,3 +14,6 @@ path = "src/lib.rs"
|
||||
cdrustanoia_core.workspace = true
|
||||
cdrustanoia_capi_types.workspace = true
|
||||
libc.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
@@ -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"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ static void use_api(cdrom_drive *d, cdrom_paranoia *p)
|
||||
{
|
||||
(void)cdda_version();
|
||||
(void)paranoia_version();
|
||||
#ifdef CDDA_TEST
|
||||
(void)cdda_identify_test("", 0, 0);
|
||||
#endif
|
||||
(void)cdda_open(d);
|
||||
(void)cdda_read(d, 0, 0, 1);
|
||||
(void)paranoia_init(d);
|
||||
|
||||
Reference in New Issue
Block a user