first draft

This commit is contained in:
tux
2026-11-03 20:19:21 +01:00
commit 158e115242
44 changed files with 10785 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "cdrustanoia_capi_types"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[lib]
name = "cdrustanoia_capi_types"
path = "src/lib.rs"
[dependencies]
libc.workspace = true
+165
View File
@@ -0,0 +1,165 @@
//! C ABI-compatible public structures shared by the two cdda libraries.
use libc::{c_char, c_int, c_long, c_void, sigset_t};
pub const MAXTRK: usize = 100;
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub struct Toc {
pub b_flags: u8,
pub b_track: u8,
pub dw_start_sector: i32,
}
pub type EnableCddaFn = Option<unsafe extern "C" fn(*mut CdromDrive, c_int) -> c_int>;
pub type ReadTocFn = Option<unsafe extern "C" fn(*mut CdromDrive) -> c_int>;
pub type ReadAudioFn =
Option<unsafe extern "C" fn(*mut CdromDrive, *mut c_void, c_long, c_long) -> c_long>;
pub type SetSpeedFn = Option<unsafe extern "C" fn(*mut CdromDrive, c_int) -> c_int>;
#[repr(C)]
pub struct CddaPrivateData {
pub holder: *mut c_void,
pub last_milliseconds: c_int,
pub clock: c_int,
}
#[repr(C)]
pub struct CdromDrive {
pub opened: c_int,
pub cdda_device_name: *mut c_char,
pub ioctl_device_name: *mut c_char,
pub cdda_fd: c_int,
pub ioctl_fd: c_int,
pub drive_model: *mut c_char,
pub drive_type: c_int,
pub interface: c_int,
pub bigendianp: c_int,
pub nsectors: c_int,
pub cd_extra: c_int,
pub tracks: c_int,
pub disc_toc: [Toc; MAXTRK],
pub audio_first_sector: c_long,
pub audio_last_sector: c_long,
pub errordest: c_int,
pub messagedest: c_int,
pub errorbuf: *mut c_char,
pub messagebuf: *mut c_char,
pub enable_cdda: EnableCddaFn,
pub read_toc: ReadTocFn,
pub read_audio: ReadAudioFn,
pub set_speed: SetSpeedFn,
pub error_retry: c_int,
pub report_all: c_int,
pub is_atapi: c_int,
pub is_mmc: c_int,
pub private: *mut CddaPrivateData,
pub reserved: *mut c_void,
pub inqbytes: [u8; 4],
pub density: u8,
pub orgdens: u8,
pub orgsize: u32,
pub bigbuff: c_long,
pub adjust_ssize: c_int,
pub fua: c_int,
pub lun: c_int,
pub sigset: sigset_t,
}
impl CdromDrive {
pub fn empty() -> Self {
Self {
opened: 0,
cdda_device_name: std::ptr::null_mut(),
ioctl_device_name: std::ptr::null_mut(),
cdda_fd: -1,
ioctl_fd: -1,
drive_model: std::ptr::null_mut(),
drive_type: 0,
interface: 0,
bigendianp: -1,
nsectors: -1,
cd_extra: 0,
tracks: 0,
disc_toc: [Toc::default(); MAXTRK],
audio_first_sector: 0,
audio_last_sector: 0,
errordest: 0,
messagedest: 0,
errorbuf: std::ptr::null_mut(),
messagebuf: std::ptr::null_mut(),
enable_cdda: None,
read_toc: None,
read_audio: None,
set_speed: None,
error_retry: 0,
report_all: 0,
is_atapi: 0,
is_mmc: 0,
private: std::ptr::null_mut(),
reserved: std::ptr::null_mut(),
inqbytes: [0; 4],
density: 0,
orgdens: 0,
orgsize: 0,
bigbuff: 0,
adjust_ssize: 1,
fua: 0,
lun: 0,
sigset: unsafe { std::mem::zeroed() },
}
}
}
pub const INTERFACE_GENERIC_SCSI: c_int = 0;
pub const INTERFACE_COOKED_IOCTL: c_int = 1;
pub const INTERFACE_TEST: c_int = 2;
pub const INTERFACE_SGIO_SCSI: c_int = 3;
pub const INTERFACE_SGIO_SCSI_BUGGY1: c_int = 4;
pub const CDDA_MESSAGE_FORGETIT: c_int = 0;
pub const CDDA_MESSAGE_PRINTIT: c_int = 1;
pub const CDDA_MESSAGE_LOGIT: c_int = 2;
#[cfg(test)]
mod tests {
use super::*;
use std::mem::{offset_of, size_of};
#[test]
fn abi_layout_matches_cdparanoia() {
assert_eq!(size_of::<Toc>(), 8);
assert_eq!(offset_of!(Toc, b_flags), 0);
assert_eq!(offset_of!(Toc, b_track), 1);
assert_eq!(offset_of!(Toc, dw_start_sector), 4);
assert_eq!(size_of::<CdromDrive>(), 1136);
assert_eq!(offset_of!(CdromDrive, opened), 0);
assert_eq!(offset_of!(CdromDrive, cdda_device_name), 8);
assert_eq!(offset_of!(CdromDrive, cdda_fd), 24);
assert_eq!(offset_of!(CdromDrive, drive_model), 32);
assert_eq!(offset_of!(CdromDrive, drive_type), 40);
assert_eq!(offset_of!(CdromDrive, interface), 44);
assert_eq!(offset_of!(CdromDrive, bigendianp), 48);
assert_eq!(offset_of!(CdromDrive, nsectors), 52);
assert_eq!(offset_of!(CdromDrive, cd_extra), 56);
assert_eq!(offset_of!(CdromDrive, tracks), 60);
assert_eq!(offset_of!(CdromDrive, disc_toc), 64);
assert_eq!(offset_of!(CdromDrive, audio_first_sector), 864);
assert_eq!(offset_of!(CdromDrive, errordest), 880);
assert_eq!(offset_of!(CdromDrive, errorbuf), 888);
assert_eq!(offset_of!(CdromDrive, error_retry), 936);
assert_eq!(offset_of!(CdromDrive, report_all), 940);
assert_eq!(offset_of!(CdromDrive, is_atapi), 944);
assert_eq!(offset_of!(CdromDrive, is_mmc), 948);
assert_eq!(offset_of!(CdromDrive, private), 952);
assert_eq!(offset_of!(CdromDrive, reserved), 960);
assert_eq!(offset_of!(CdromDrive, inqbytes), 968);
assert_eq!(offset_of!(CdromDrive, density), 972);
assert_eq!(offset_of!(CdromDrive, bigbuff), 984);
assert_eq!(offset_of!(CdromDrive, adjust_ssize), 992);
assert_eq!(offset_of!(CdromDrive, fua), 996);
assert_eq!(offset_of!(CdromDrive, lun), 1000);
assert_eq!(offset_of!(CdromDrive, sigset), 1008);
}
}
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "cdda_interface"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[lib]
name = "cdda_interface"
crate-type = ["cdylib", "staticlib", "rlib"]
path = "src/lib.rs"
[dependencies]
cdrustanoia_core.workspace = true
cdrustanoia_capi_types.workspace = true
libc.workspace = true
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libcdda_interface.so.0");
}
+571
View File
@@ -0,0 +1,571 @@
#![allow(
clippy::missing_safety_doc,
clippy::unnecessary_cast,
clippy::field_reassign_with_default
)]
//! C ABI-compatible libcdda_interface implementation.
use cdrustanoia_capi_types::*;
use cdrustanoia_core::device::discover;
use cdrustanoia_core::device::{DiscToc, Drive, InterfaceKind, ReadError};
use libc::{c_char, c_int, c_long, c_void};
use std::ffi::CString;
use std::ptr;
pub struct BackendHolder {
pub drive: Box<dyn Drive>,
}
unsafe fn cstr(s: &str) -> *mut c_char {
let clean: String = s.chars().filter(|c| *c != '\0').collect();
match CString::new(clean) {
Ok(c) => libc::strdup(c.as_ptr()),
Err(_) => ptr::null_mut(),
}
}
unsafe fn set_error(d: *mut CdromDrive, message: &str) {
if d.is_null() {
return;
}
if !(*d).errorbuf.is_null() {
libc::free((*d).errorbuf as *mut c_void);
}
(*d).errorbuf = cstr(message);
}
pub unsafe fn holder<'a>(d: *mut CdromDrive) -> Option<&'a mut BackendHolder> {
if d.is_null() {
return None;
}
let private = (*d).private;
if private.is_null() {
return None;
}
let h = (*private).holder as *mut BackendHolder;
if h.is_null() {
None
} else {
Some(&mut *h)
}
}
pub unsafe fn make_drive(
device: &str,
ioctl_device: Option<&str>,
backend: Box<dyn Drive>,
) -> *mut CdromDrive {
let mut d = Box::new(CdromDrive::empty());
d.drive_type = 0;
d.interface = match backend.interface() {
InterfaceKind::SgIo => INTERFACE_SGIO_SCSI,
InterfaceKind::Cooked => INTERFACE_COOKED_IOCTL,
InterfaceKind::Image => INTERFACE_TEST,
};
d.bigendianp = backend.bigendianp();
d.nsectors = backend.nsectors() as c_int;
d.cdda_device_name = cstr(device);
d.ioctl_device_name = cstr(ioctl_device.unwrap_or(device));
d.drive_model = cstr(backend.drive_model());
d.enable_cdda = Some(cb_enable_cdda);
d.read_toc = Some(cb_read_toc);
d.read_audio = Some(cb_read_audio);
d.set_speed = Some(cb_set_speed);
d.error_retry = 1;
d.report_all = 0;
d.adjust_ssize = 1;
d.lun = 0;
let private = Box::new(CddaPrivateData {
holder: Box::into_raw(Box::new(BackendHolder { drive: backend })) as *mut c_void,
last_milliseconds: -1,
clock: 0,
});
d.private = Box::into_raw(private);
Box::into_raw(d)
}
unsafe fn free_drive(d: *mut CdromDrive) {
if d.is_null() {
return;
}
let private = (*d).private;
if !private.is_null() {
let holder = (*private).holder as *mut BackendHolder;
if !holder.is_null() {
drop(Box::from_raw(holder));
}
drop(Box::from_raw(private));
(*d).private = ptr::null_mut();
}
if !(*d).cdda_device_name.is_null() {
libc::free((*d).cdda_device_name as *mut c_void);
}
if !(*d).ioctl_device_name.is_null() {
libc::free((*d).ioctl_device_name as *mut c_void);
}
if !(*d).drive_model.is_null() {
libc::free((*d).drive_model as *mut c_void);
}
if !(*d).errorbuf.is_null() {
libc::free((*d).errorbuf as *mut c_void);
}
if !(*d).messagebuf.is_null() {
libc::free((*d).messagebuf as *mut c_void);
}
drop(Box::from_raw(d));
}
unsafe extern "C" fn cb_enable_cdda(_d: *mut CdromDrive, _onoff: c_int) -> c_int {
0
}
unsafe extern "C" fn cb_read_toc(d: *mut CdromDrive) -> c_int {
if d.is_null() {
return -1;
}
(*d).tracks
}
unsafe fn read_into(
d: *mut CdromDrive,
p: *mut c_void,
begin: c_long,
sectors: c_long,
) -> Result<usize, ReadError> {
let h = holder(d).ok_or_else(ReadError::closed)?;
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 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)
}
}
unsafe extern "C" fn cb_read_audio(
d: *mut CdromDrive,
p: *mut c_void,
begin: c_long,
sectors: c_long,
) -> c_long {
match read_into(d, p, begin, sectors) {
Ok(n) => n as c_long,
Err(e) => {
set_errno(e.errno);
match e.errno {
libc::ENOMEDIUM => -404,
libc::ENOMEM => -300,
_ => -7,
}
}
}
}
unsafe extern "C" fn cb_set_speed(d: *mut CdromDrive, speed: c_int) -> c_int {
match holder(d) {
Some(h) => h.drive.speed_set(speed),
None => -405,
}
}
fn set_errno(value: c_int) {
unsafe {
*libc::__errno_location() = value;
}
}
fn toc_from_drive(d: &CdromDrive) -> DiscToc {
let mut toc = DiscToc::default();
toc.tracks = d.tracks;
toc.cd_extra = d.cd_extra;
for i in 0..MAXTRK {
toc.entries[i].flags = d.disc_toc[i].b_flags;
toc.entries[i].track = d.disc_toc[i].b_track;
toc.entries[i].start_sector = d.disc_toc[i].dw_start_sector as i64;
}
toc
}
unsafe fn fill_toc(d: *mut CdromDrive) {
let h = match holder(d) {
Some(h) => h,
None => return,
};
let toc = h.drive.toc().clone();
(*d).tracks = toc.tracks;
(*d).cd_extra = toc.cd_extra;
for i in 0..MAXTRK {
(*d).disc_toc[i].b_flags = toc.entries[i].flags;
(*d).disc_toc[i].b_track = toc.entries[i].track;
(*d).disc_toc[i].dw_start_sector = toc.entries[i].start_sector as i32;
}
(*d).audio_first_sector = toc.disc_firstsector() as c_long;
(*d).audio_last_sector = toc.disc_lastsector() as c_long;
(*d).bigendianp = h.drive.bigendianp();
(*d).nsectors = h.drive.nsectors() as c_int;
}
#[no_mangle]
pub unsafe extern "C" fn cdda_identify(
device: *const c_char,
messagedest: c_int,
messages: *mut *mut c_char,
) -> *mut CdromDrive {
if !messages.is_null() {
*messages = ptr::null_mut();
}
if device.is_null() {
return ptr::null_mut();
}
let device = match std::ffi::CStr::from_ptr(device).to_str() {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
match discover::identify_device(device, messagedest == CDDA_MESSAGE_PRINTIT) {
Ok(drive) => make_drive(device, Some(device), drive),
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn cdda_identify_cooked(
device: *const c_char,
messagedest: c_int,
messages: *mut *mut c_char,
) -> *mut CdromDrive {
if !messages.is_null() {
*messages = ptr::null_mut();
}
if device.is_null() {
return ptr::null_mut();
}
let device = match std::ffi::CStr::from_ptr(device).to_str() {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
match discover::identify_cooked(device, messagedest == CDDA_MESSAGE_PRINTIT) {
Ok(drive) => make_drive(device, Some(device), drive),
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn cdda_identify_scsi(
generic_device: *const c_char,
ioctl_device: *const c_char,
messagedest: c_int,
messages: *mut *mut c_char,
) -> *mut CdromDrive {
if !messages.is_null() {
*messages = ptr::null_mut();
}
let generic = if generic_device.is_null() {
None
} else {
std::ffi::CStr::from_ptr(generic_device).to_str().ok()
};
let ioctl = if ioctl_device.is_null() {
None
} else {
std::ffi::CStr::from_ptr(ioctl_device).to_str().ok()
};
let verbose = messagedest == CDDA_MESSAGE_PRINTIT;
if let Some(dev) = generic {
if let Ok(drive) = discover::identify_sgio(dev, verbose) {
return make_drive(dev, ioctl, drive);
}
}
if let Some(dev) = ioctl {
if let Ok(drive) = discover::identify_device(dev, verbose) {
return make_drive(dev, Some(dev), drive);
}
}
ptr::null_mut()
}
#[no_mangle]
pub unsafe extern "C" fn cdda_find_a_cdrom(
messagedest: c_int,
messages: *mut *mut c_char,
) -> *mut CdromDrive {
if !messages.is_null() {
*messages = ptr::null_mut();
}
match discover::find_a_cdrom(messagedest == CDDA_MESSAGE_PRINTIT) {
Some(drive) => make_drive("auto", None, drive),
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn cdda_open(d: *mut CdromDrive) -> c_int {
if d.is_null() {
return -100;
}
if (*d).opened != 0 {
return 0;
}
if let Some(h) = holder(d) {
if let Err(e) = h.drive.open_disc() {
set_errno(libc::EIO);
set_error(d, &format!("Unable to open disc: {}\n", e));
return if e.contains("illegal number of tracks") {
-3
} else if e.contains("contents header") {
-4
} else if e.contains("contents entry") {
-5
} else if e.contains("lead-out") {
-2
} else if e.contains("CDDA") || e.contains("command set") {
-6
} else {
-9
};
}
}
fill_toc(d);
if (*d).tracks < 1 {
(*d).opened = 0;
return -9;
}
for i in 0..(*d).tracks as usize {
if (*d).disc_toc[i].dw_start_sector < 0 || (*d).disc_toc[i + 1].dw_start_sector == 0 {
(*d).opened = 0;
return -9;
}
}
(*d).opened = 1;
0
}
#[no_mangle]
pub unsafe extern "C" fn cdda_close(d: *mut CdromDrive) -> c_int {
if d.is_null() {
return 0;
}
free_drive(d);
0
}
#[no_mangle]
pub unsafe extern "C" fn cdda_read_timed(
d: *mut CdromDrive,
buffer: *mut c_void,
beginsector: c_long,
sectors: c_long,
ms: *mut c_int,
) -> c_long {
if !ms.is_null() {
*ms = -1;
}
if d.is_null() || (*d).opened == 0 {
set_errno(libc::EBADF);
set_error(d, "400: Device not open\n");
return -400;
}
if sectors <= 0 {
return sectors;
}
let h = match holder(d) {
Some(h) => h,
None => {
set_errno(libc::EBADF);
return -400;
}
};
if (*d).bigendianp >= 0 {
h.drive.set_bigendianp((*d).bigendianp);
}
if (*d).nsectors > 0 {
h.drive.set_nsectors((*d).nsectors as i64);
}
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 buf = std::slice::from_raw_parts_mut(buffer as *mut u8, len);
h.drive
.read_timed(Some(buf), beginsector as i64, sectors as i64)
};
if !ms.is_null() {
*ms = timed.milliseconds;
}
if (*d).bigendianp < 0 {
(*d).bigendianp = h.drive.bigendianp();
}
match timed.result {
Ok(n) => n as c_long,
Err(e) => {
set_errno(e.errno);
set_error(d, &format!("SCSI read error: {}\n", e.message));
match e.errno {
libc::ENOMEDIUM => -404,
libc::ENOMEM => -300,
_ => -7,
}
}
}
}
#[no_mangle]
pub unsafe extern "C" fn cdda_read(
d: *mut CdromDrive,
buffer: *mut c_void,
beginsector: c_long,
sectors: c_long,
) -> c_long {
cdda_read_timed(d, buffer, beginsector, sectors, ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn cdda_speed_set(d: *mut CdromDrive, speed: c_int) -> c_int {
match holder(d) {
Some(h) => {
let ret = h.drive.speed_set(speed);
if ret == 0 {
0
} else {
-405
}
}
None => -405,
}
}
#[no_mangle]
pub unsafe extern "C" fn cdda_verbose_set(
d: *mut CdromDrive,
err_action: c_int,
mes_action: c_int,
) {
if !d.is_null() {
(*d).errordest = err_action;
(*d).messagedest = mes_action;
}
}
#[no_mangle]
pub unsafe extern "C" fn cdda_messages(d: *mut CdromDrive) -> *mut c_char {
if d.is_null() {
return ptr::null_mut();
}
let ret = (*d).messagebuf;
(*d).messagebuf = ptr::null_mut();
ret
}
#[no_mangle]
pub unsafe extern "C" fn cdda_errors(d: *mut CdromDrive) -> *mut c_char {
if d.is_null() {
return ptr::null_mut();
}
let ret = (*d).errorbuf;
(*d).errorbuf = ptr::null_mut();
ret
}
#[no_mangle]
pub extern "C" fn cdda_version() -> *const c_char {
c"10.2".as_ptr()
}
#[no_mangle]
pub unsafe extern "C" fn cdda_tracks(d: *mut CdromDrive) -> c_long {
if d.is_null() || (*d).opened == 0 {
return -400;
}
(*d).tracks as c_long
}
#[no_mangle]
pub unsafe extern "C" fn cdda_track_firstsector(d: *mut CdromDrive, track: c_int) -> c_long {
if d.is_null() || (*d).opened == 0 {
return -400;
}
toc_from_drive(&*d).track_firstsector(track) as c_long
}
#[no_mangle]
pub unsafe extern "C" fn cdda_track_lastsector(d: *mut CdromDrive, track: c_int) -> c_long {
if d.is_null() || (*d).opened == 0 {
return -400;
}
toc_from_drive(&*d).track_lastsector(track) as c_long
}
#[no_mangle]
pub unsafe extern "C" fn cdda_disc_firstsector(d: *mut CdromDrive) -> c_long {
if d.is_null() || (*d).opened == 0 {
return -400;
}
toc_from_drive(&*d).disc_firstsector() as c_long
}
#[no_mangle]
pub unsafe extern "C" fn cdda_disc_lastsector(d: *mut CdromDrive) -> c_long {
if d.is_null() || (*d).opened == 0 {
return -400;
}
toc_from_drive(&*d).disc_lastsector() as c_long
}
#[no_mangle]
pub unsafe extern "C" fn cdda_sector_gettrack(d: *mut CdromDrive, sector: c_long) -> c_int {
if d.is_null() || (*d).opened == 0 {
return -400;
}
toc_from_drive(&*d).sector_gettrack(sector as i64)
}
#[no_mangle]
pub unsafe extern "C" fn cdda_track_channels(d: *mut CdromDrive, track: c_int) -> c_int {
if d.is_null() || (*d).opened == 0 {
return -400;
}
toc_from_drive(&*d).track_channels(track)
}
#[no_mangle]
pub unsafe extern "C" fn cdda_track_audiop(d: *mut CdromDrive, track: c_int) -> c_int {
if d.is_null() || (*d).opened == 0 {
return -400;
}
toc_from_drive(&*d).track_audiop(track)
}
#[no_mangle]
pub unsafe extern "C" fn cdda_track_copyp(d: *mut CdromDrive, track: c_int) -> c_int {
if d.is_null() || (*d).opened == 0 {
return -400;
}
toc_from_drive(&*d).track_copyp(track)
}
#[no_mangle]
pub unsafe extern "C" fn cdda_track_preemp(d: *mut CdromDrive, track: c_int) -> c_int {
if d.is_null() || (*d).opened == 0 {
return -400;
}
toc_from_drive(&*d).track_preemp(track)
}
#[no_mangle]
pub unsafe extern "C" fn cdda_track_bitmap(
d: *mut CdromDrive,
track: c_int,
bit: c_int,
set: c_int,
clear: c_int,
) -> c_int {
if d.is_null() || (*d).opened == 0 {
return -400;
}
toc_from_drive(&*d).track_bitmap(track, bit as u8, set, clear)
}
// Keep the c_long import used on all targets.
#[allow(dead_code)]
fn _typecheck(_: c_long) {}
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "cdda_paranoia"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[lib]
name = "cdda_paranoia"
crate-type = ["cdylib", "staticlib", "rlib"]
path = "src/lib.rs"
[dependencies]
cdrustanoia_core.workspace = true
cdrustanoia_capi_types.workspace = true
libc.workspace = true
[dev-dependencies]
cdda_interface = { path = "../cdda-interface" }
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libcdda_paranoia.so.0");
}
+314
View File
@@ -0,0 +1,314 @@
#![allow(
clippy::missing_safety_doc,
clippy::unnecessary_cast,
clippy::field_reassign_with_default
)]
//! C ABI-compatible libcdda_paranoia implementation.
use cdrustanoia_capi_types::*;
use cdrustanoia_core::device::{
host_big_endian, swap_raw16, DiscToc, Drive, InterfaceKind, ReadError, TimedRead,
CD_FRAMESIZE_RAW, MAXTRK,
};
use cdrustanoia_core::paranoia::Paranoia;
use libc::{c_char, c_int, c_long, c_void};
use std::ffi::CStr;
use std::ptr;
use std::time::Instant;
pub type ParanoiaCallback = Option<unsafe extern "C" fn(c_long, c_int)>;
struct CAbiDrive {
d: *mut CdromDrive,
toc: DiscToc,
model: String,
bigendianp: i32,
nsectors: i64,
interface: InterfaceKind,
}
unsafe fn toc_from_d(d: *mut CdromDrive) -> DiscToc {
let mut toc = DiscToc::default();
if d.is_null() {
return toc;
}
toc.tracks = (*d).tracks;
toc.cd_extra = (*d).cd_extra;
for i in 0..MAXTRK {
toc.entries[i].flags = (*d).disc_toc[i].b_flags;
toc.entries[i].track = (*d).disc_toc[i].b_track;
toc.entries[i].start_sector = (*d).disc_toc[i].dw_start_sector as i64;
}
toc
}
impl CAbiDrive {
unsafe fn new(d: *mut CdromDrive) -> Self {
let model = if (*d).drive_model.is_null() {
String::new()
} else {
CStr::from_ptr((*d).drive_model)
.to_string_lossy()
.into_owned()
};
let interface = match (*d).interface {
INTERFACE_COOKED_IOCTL => InterfaceKind::Cooked,
INTERFACE_TEST => InterfaceKind::Image,
_ => InterfaceKind::SgIo,
};
Self {
d,
toc: toc_from_d(d),
model,
bigendianp: (*d).bigendianp,
nsectors: (*d).nsectors as i64,
interface,
}
}
fn refresh(&mut self) {
unsafe {
self.toc = toc_from_d(self.d);
self.bigendianp = (*self.d).bigendianp;
self.nsectors = (*self.d).nsectors as i64;
}
}
}
impl Drive for CAbiDrive {
fn toc(&self) -> &DiscToc {
&self.toc
}
fn toc_mut(&mut self) -> &mut DiscToc {
&mut self.toc
}
fn opened(&self) -> bool {
unsafe { !self.d.is_null() && (*self.d).opened != 0 }
}
fn read_audio(
&mut self,
mut buffer: Option<&mut [u8]>,
begin: i64,
sectors: i64,
) -> Result<usize, ReadError> {
unsafe {
if self.d.is_null() || (*self.d).opened == 0 {
return Err(ReadError::closed());
}
let f = (*self.d)
.read_audio
.ok_or_else(|| ReadError::new(libc::EIO, "No read function"))?;
let ptr = match buffer.as_mut() {
Some(b) => b.as_mut_ptr() as *mut c_void,
None => ptr::null_mut(),
};
let ret = f(self.d, ptr, begin as c_long, sectors as c_long);
if ret < 0 {
let errno = match ret {
-404 => libc::ENOMEDIUM,
-300 => libc::ENOMEM,
_ => libc::EIO,
};
return Err(ReadError::new(errno, "SCSI read error"));
}
let n = ret as usize;
if let Some(buf) = buffer.as_mut() {
let be = (*self.d).bigendianp;
if n > 0 && be >= 0 && (be == 1) != host_big_endian() {
let len = (n * CD_FRAMESIZE_RAW).min(buf.len());
swap_raw16(&mut buf[..len]);
}
}
Ok(n)
}
}
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);
TimedRead {
result,
milliseconds: started.elapsed().as_millis() as i32,
}
}
fn speed_set(&mut self, speed: i32) -> i32 {
unsafe {
if self.d.is_null() {
return -405;
}
match (*self.d).set_speed {
Some(f) => f(self.d, speed as c_int),
None => -405,
}
}
}
fn close(&mut self) {
// The paranoia library never closes the caller-owned drive.
}
fn drive_model(&self) -> &str {
&self.model
}
fn bigendianp(&self) -> i32 {
self.bigendianp
}
fn set_bigendianp(&mut self, value: i32) {
self.bigendianp = value;
unsafe {
if !self.d.is_null() {
(*self.d).bigendianp = value;
}
}
}
fn nsectors(&self) -> i64 {
self.nsectors
}
fn set_nsectors(&mut self, value: i64) {
self.nsectors = value;
unsafe {
if !self.d.is_null() {
(*self.d).nsectors = value as c_int;
}
}
}
fn interface(&self) -> InterfaceKind {
self.interface
}
}
unsafe fn p_ref<'a>(p: *mut c_void) -> Option<&'a mut Paranoia> {
if p.is_null() {
None
} else {
Some(&mut *(p as *mut Paranoia))
}
}
#[no_mangle]
pub unsafe extern "C" fn paranoia_version() -> *const c_char {
c"10.2".as_ptr()
}
#[no_mangle]
pub unsafe extern "C" fn paranoia_init(d: *mut CdromDrive) -> *mut c_void {
if d.is_null() {
return ptr::null_mut();
}
let mut adapter = CAbiDrive::new(d);
adapter.refresh();
let p = Paranoia::new(Box::new(adapter));
Box::into_raw(Box::new(p)) as *mut c_void
}
#[no_mangle]
pub unsafe extern "C" fn paranoia_free(p: *mut c_void) {
if !p.is_null() {
drop(Box::from_raw(p as *mut Paranoia));
}
}
#[no_mangle]
pub unsafe extern "C" fn paranoia_modeset(p: *mut c_void, mode: c_int) {
if let Some(p) = p_ref(p) {
p.modeset(mode);
}
}
#[no_mangle]
pub unsafe extern "C" fn paranoia_seek(p: *mut c_void, seek: c_long, mode: c_int) -> c_long {
match p_ref(p) {
Some(p) => p.seek(seek as i64, mode) as c_long,
None => -1,
}
}
#[no_mangle]
pub unsafe extern "C" fn paranoia_overlapset(p: *mut c_void, overlap: c_long) {
if let Some(p) = p_ref(p) {
p.overlapset(overlap as i64);
}
}
#[no_mangle]
pub unsafe extern "C" fn paranoia_cachemodel_size(p: *mut c_void, sectors: c_int) -> c_int {
match p_ref(p) {
Some(p) => p.cachemodel_size(sectors as i64) as c_int,
None => 0,
}
}
#[no_mangle]
pub unsafe extern "C" fn paranoia_read(p: *mut c_void, callback: ParanoiaCallback) -> *mut i16 {
paranoia_read_limited(p, callback, 20)
}
#[no_mangle]
pub unsafe extern "C" fn paranoia_read_limited(
p: *mut c_void,
callback: ParanoiaCallback,
max_retries: c_int,
) -> *mut i16 {
let p = match p_ref(p) {
Some(p) => p,
None => return ptr::null_mut(),
};
let mut cb = |pos: i64, func: i32| {
if let Some(f) = callback {
f(pos as c_long, func as c_int);
}
};
match p.read_limited(&mut cb, max_retries) {
Some(slice) => slice.as_mut_ptr(),
None => {
let errno = if p.drive().opened() {
libc::EIO
} else {
libc::EBADF
};
*libc::__errno_location() = errno;
ptr::null_mut()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use cdrustanoia_core::device::image::ImageDrive;
use cdrustanoia_core::device::samples_to_bytes;
use cdrustanoia_core::device::CD_FRAMEWORDS;
#[test]
fn c_api_query_and_read() {
unsafe {
let mut samples = Vec::new();
for i in 0..(3 * 1176) {
samples.push(((i * 37) as i16).wrapping_mul(3));
}
let bytes = samples_to_bytes(&samples);
let img = ImageDrive::from_bytes(bytes);
let d = cdda_interface::make_drive("test", None, Box::new(img));
assert_eq!(cdda_interface::cdda_open(d), 0);
assert_eq!(cdda_interface::cdda_tracks(d), 1);
assert_eq!(cdda_interface::cdda_disc_firstsector(d), 0);
assert_eq!(cdda_interface::cdda_disc_lastsector(d), 2);
let mut raw = vec![0u8; CD_FRAMESIZE_RAW];
let n = cdda_interface::cdda_read(d, raw.as_mut_ptr() as *mut c_void, 0, 1);
assert_eq!(n, 1);
assert_eq!(raw, samples_to_bytes(&samples[..1176]));
let p = paranoia_init(d);
assert!(!p.is_null());
paranoia_modeset(p, cdrustanoia_core::paranoia::PARANOIA_MODE_FULL);
paranoia_seek(p, 0, 0);
for sector in 0..3 {
let ptr = paranoia_read(p, None);
assert!(!ptr.is_null());
let got = std::slice::from_raw_parts(ptr, CD_FRAMEWORDS as usize);
assert_eq!(got, &samples[sector * 1176..(sector + 1) * 1176]);
}
paranoia_free(p);
cdda_interface::cdda_close(d);
}
}
}
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "cdrustanoia_cli"
version.workspace = true
edition.workspace = true
license = "GPL-2.0-or-later"
publish = false
[[bin]]
name = "cdrustanoia"
path = "src/main.rs"
[[bin]]
name = "cdparanoia"
path = "src/bin_cdparanoia.rs"
[lib]
name = "cdrustanoia_cli"
path = "src/lib.rs"
[dependencies]
cdrustanoia_core.workspace = true
libc.workspace = true
[dev-dependencies]
tempfile = "3"
+295
View File
@@ -0,0 +1,295 @@
//! Drive cache/timing analysis (`-A`), based on cachetest.c.
//!
//! This is a compact but functional port: it measures seek/read timing,
//! estimates the random-access cache size, checks cache transfer speed, and
//! verifies that a backward seek flushes the cache. Output wording follows
//! the original where it matters to users.
use crate::log::Log;
use crate::{log_c, print_c, report_c};
use cdrustanoia_core::device::{Drive, CD_FRAMESIZE_RAW};
use std::thread::sleep;
use std::time::Duration;
const MIN_SEEK_MS: i32 = 6;
fn read_timed(d: &mut dyn Drive, lba: i64, len: i64) -> (i64, i32) {
let t = d.read_timed(None, lba, len);
match t.result {
Ok(n) => (n as i64, t.milliseconds),
Err(e) => {
if e.errno == libc::ENOMEDIUM {
(-404, -1)
} else {
(-1, -1)
}
}
}
}
fn time_drive(d: &mut dyn Drive, log: &mut Log, lba: i64, len: i64, initial_seek: bool) -> i64 {
log_c!(log, "\n");
let mut sofar = 0i64;
let mut sum = 0f64;
let mut sumsq = 0f64;
let mut latency = 0i32;
let mut i = 0;
while sofar < len {
let toread = if i == 0 && initial_seek {
1
} else {
len - sofar
};
let (ret, x) = read_timed(d, lba + sofar, toread);
if ret <= 0 {
return if ret == -404 { -404 } else { -1 };
}
let x = x.clamp(0, 9999);
log_c!(log, "{}:{}:{} ", lba + sofar, ret, x);
sofar += ret;
if i != 0 || !initial_seek {
sum += x as f64;
sumsq += (x * x) as f64 / ret as f64;
} else {
latency = x;
}
i += 1;
}
let denom = (len - 1).max(1) as f64;
let mean = sum / denom;
let stddev = ((sumsq / denom - mean * mean).max(0.0)).sqrt();
if initial_seek {
print_c!(
log,
"{:4}ms seek, {:.2}ms/sec read [{:.1}x]",
latency,
mean,
1000.0 / 75.0 / mean.max(0.001)
);
log_c!(
log,
"\n\tInitial seek latency ({} sectors): {}ms",
len,
latency
);
}
log_c!(
log,
"\n\tAverage read latency: {:.2}ms/sector (raw speed: {:.1}x)",
mean,
1000.0 / 75.0 / mean.max(0.001)
);
log_c!(
log,
"\n\tRead latency standard deviation: {:.2}ms/sector",
stddev
);
sum as i64
}
fn retime_drive(d: &mut dyn Drive, log: &mut Log, lba: i64, readahead: i64, oldmean: f64) -> f64 {
let mut sectors = 2000i64;
if sectors as f64 * oldmean > 5000.0 {
sectors = (5000.0 / oldmean.max(0.001)) as i64;
}
let readahead = readahead * 10 / 9;
if readahead > sectors {
sectors = readahead;
}
let total = time_drive(d, log, lba, sectors, true);
if total <= 0 {
return oldmean;
}
let newmean = total as f64 / sectors as f64;
log_c!(
log,
"\n\tOld mean={:.2}ms/sec, New mean={:.2}ms/sec\n",
oldmean,
newmean
);
newmean.max(oldmean)
}
#[allow(unused_assignments)]
pub fn analyze_cache(d: &mut dyn Drive, log: &mut Log, speed: i32) -> i32 {
let speed = if speed <= 0 { -1 } else { speed };
report_c!(
log,
"\n=================== Checking drive cache/timing behavior ===================\n"
);
// Longest contiguous audio stretch.
let mut firstsector = -1i64;
let mut lastsector = -1i64;
let mut firsttest = -1i64;
let mut lasttest = -1i64;
for i in 1..=d.tracks() {
if d.track_audiop(i) {
if firsttest == -1 {
firsttest = d.track_firstsector(i);
}
lasttest = d.track_lastsector(i);
if lasttest - firsttest > lastsector - firstsector {
firstsector = firsttest;
lastsector = lasttest;
}
} else {
firsttest = -1;
}
}
if firstsector == -1 {
report_c!(
log,
"\n\tNo audio on disc; Cannot determine timing behavior..."
);
return -1;
}
let avail = lastsector - firstsector + 1;
let current = (1000i64).min((avail / 2).max(1));
let mut offset = firstsector + 10;
if offset + current > lastsector {
offset = firstsector;
}
report_c!(log, "\nSeek/read timing:\n");
let total = time_drive(d, log, offset, current, true);
if total == -404 {
return -1;
}
if total > 0 {
let mspersector = total as f64 / current as f64;
report_c!(
log,
"\n\tAverage access speed: {:.2}ms/sector [{:.1}x]",
mspersector,
1000.0 / 75.0 / mspersector.max(0.001)
);
}
report_c!(log, "\n\nAnalyzing cache behavior...\n");
// Find the end of the drive's random-access cache by preloading a
// linear run and timing a read of its first sector.
let mut cache_size = 0i64;
let mut c = 1i64;
while c <= 1200 {
let (r1, _) = read_timed(d, offset + c, 1);
if r1 <= 0 {
offset += c + 100;
if offset + c > lastsector {
break;
}
continue;
}
let (r2, ms) = read_timed(d, offset, 1);
if r2 <= 0 {
offset += c + 100;
if offset + c > lastsector {
break;
}
continue;
}
if ms >= MIN_SEEK_MS {
break;
}
cache_size = c;
c += 64;
}
if cache_size == 0 {
report_c!(
log,
"\tDrive does not cache nonlinear access \n"
);
return 0;
}
report_c!(
log,
"\tApproximate random access cache size: {} sector(s) \n",
cache_size
);
let mut warn = 0;
if cache_size > 1200 {
report_c!(
log,
"\nWARNING: This drive appears to be caching more sectors of\n\
readahead than Paranoia can currently handle!\n"
);
warn = 1;
}
if speed == -1 {
log_c!(log, "\tAttempting to reset read speed to full... ");
} else {
log_c!(log, "\tAttempting to reset read speed to {}... ", speed);
}
if d.speed_set(speed) != 0 {
log_c!(log, "failed.\n");
} else {
log_c!(log, "drive said OK\n");
}
// Cache transfer speed and backseek flushing.
let readsize = (cache_size - 8).max(1).min(cache_size);
if readsize >= 7 {
report_c!(log, "\tTesting cache transfer speed...");
let (_, _) = read_timed(d, offset, readsize);
let mut elapsed = 0i64;
let mut sectors = 0i64;
let start = std::time::Instant::now();
while start.elapsed().as_millis() < 1000 {
let (n, ms) = read_timed(d, offset, readsize);
if n <= 0 {
break;
}
elapsed += ms as i64;
sectors += n;
}
let cachems = if sectors > 0 {
elapsed as f64 / sectors as f64
} else {
0.0
};
report_c!(
log,
"\t Cache read speed: {:.2}ms/sector [{}x]\n",
cachems,
(1000.0 / 75.0 / cachems.max(0.001)) as i64
);
report_c!(log, "\tTesting that backseek flushes cache...");
let seekpos = (offset + cache_size + 20000).min((lastsector - 150).max(offset + 1));
let (r, _) = read_timed(d, seekpos, 1);
let mut readms = 0.0;
if r > 0 {
let total = time_drive(d, log, offset + 1, readsize, true);
if total > 0 {
readms = total as f64 / readsize as f64;
}
}
report_c!(
log,
"\t Access speed after backseek: {:.2}ms/sector [{}x]\n",
readms,
(1000.0 / 75.0 / readms.max(0.001)) as i64
);
if readms > 0.0 && readms * 2.0 < cachems.max(0.001) {
report_c!(
log,
"\tWARNING: Read timing after backseek faster than expected!\n\
\t It's possible/likely that this drive is not\n\
\t flushing the readahead cache on backward seeks!\n\n"
);
warn = 1;
} else if readms > 0.0 {
report_c!(log, "\tBackseek flushes the cache as expected\n");
}
let _ = retime_drive(d, log, offset, readsize, cachems.max(1.0));
}
// A short pause mirrors the original's readahead settling delay.
sleep(Duration::from_millis(50));
let _ = CD_FRAMESIZE_RAW;
warn
}
+420
View File
@@ -0,0 +1,420 @@
//! GNU getopt-compatible command line parsing for cdparanoia 10.2.
use crate::output::OutputFormat;
use cdrustanoia_core::paranoia::*;
#[derive(Debug, Clone)]
pub struct Args {
pub analyze: bool,
pub verbose: bool,
pub quiet: bool,
pub stderr_progress: bool,
pub log_summary: Option<Option<String>>,
pub log_debug: Option<Option<String>>,
pub version: bool,
pub query: bool,
pub batch: bool,
pub search: bool,
pub help: bool,
pub output_format: OutputFormat,
pub force_endian: Option<i32>,
pub force_sectors: Option<i32>,
pub force_overlap: Option<i32>,
pub force_device: Option<String>,
pub force_cooked_device: Option<String>,
pub force_generic_device: Option<String>,
pub force_speed: Option<i32>,
pub toc_offset: i32,
pub toc_bias: bool,
pub sample_offset: i32,
pub max_retries: i32,
pub paranoia_mode: i32,
pub abort_on_skip: bool,
pub info_file: Option<String>,
pub span: Option<String>,
pub outfile: Option<String>,
}
impl Default for Args {
fn default() -> Self {
Self {
analyze: false,
verbose: false,
quiet: false,
stderr_progress: false,
log_summary: None,
log_debug: None,
version: false,
query: false,
batch: false,
search: false,
help: false,
output_format: OutputFormat::Wav,
force_endian: None,
force_sectors: None,
force_overlap: None,
force_device: None,
force_cooked_device: None,
force_generic_device: None,
force_speed: None,
toc_offset: 0,
toc_bias: false,
sample_offset: 0,
max_retries: 20,
paranoia_mode: PARANOIA_MODE_FULL ^ PARANOIA_MODE_NEVERSKIP,
abort_on_skip: false,
info_file: None,
span: None,
outfile: None,
}
}
}
fn atoi(s: &str) -> i32 {
let s = s.trim_start();
let mut chars = s.chars().peekable();
let mut sign = 1i64;
if let Some(c) = chars.peek() {
if *c == '-' {
sign = -1;
chars.next();
} else if *c == '+' {
chars.next();
}
}
let mut value = 0i64;
let mut any = false;
for c in chars {
if let Some(d) = c.to_digit(10) {
any = true;
value = value.saturating_mul(10).saturating_add(d as i64);
} else {
break;
}
}
if !any {
return 0;
}
(sign * value) as i32
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Short {
NoArg(char),
Required(char),
Optional(char),
}
fn short_kind(c: char) -> Option<Short> {
match c {
'e' | 's' | 'c' | 'C' | 'p' | 'r' | 'R' | 'w' | 'a' | 'f' | 'v' | 'q' | 'V' | 'Q' | 'h'
| 'Z' | 'Y' | 'X' | 'W' | 'B' | 'F' | 'T' | 'A' => Some(Short::NoArg(c)),
'n' | 'o' | 'O' | 'd' | 'g' | 'k' | 'S' | 'i' | 't' => Some(Short::Required(c)),
'z' | 'l' | 'L' => Some(Short::Optional(c)),
_ => None,
}
}
fn long_kind(name: &str) -> Option<(&'static str, Short)> {
let table: &[(&str, Short)] = &[
("stderr-progress", Short::NoArg('e')),
("search-for-drive", Short::NoArg('s')),
("force-cdrom-little-endian", Short::NoArg('c')),
("force-cdrom-big-endian", Short::NoArg('C')),
("force-default-sectors", Short::Required('n')),
("force-search-overlap", Short::Required('o')),
("force-cdrom-device", Short::Required('d')),
("force-cooked-device", Short::Required('k')),
("force-generic-device", Short::Required('g')),
("force-read-speed", Short::Required('S')),
("sample-offset", Short::Required('O')),
("toc-offset", Short::Required('t')),
("toc-bias", Short::NoArg('T')),
("output-raw", Short::NoArg('p')),
("output-raw-little-endian", Short::NoArg('r')),
("output-raw-big-endian", Short::NoArg('R')),
("output-wav", Short::NoArg('w')),
("output-aiff", Short::NoArg('f')),
("output-aifc", Short::NoArg('a')),
("batch", Short::NoArg('B')),
("verbose", Short::NoArg('v')),
("quiet", Short::NoArg('q')),
("version", Short::NoArg('V')),
("query", Short::NoArg('Q')),
("help", Short::NoArg('h')),
("analyze-drive", Short::NoArg('A')),
("disable-paranoia", Short::NoArg('Z')),
("disable-extra-paranoia", Short::NoArg('Y')),
("abort-on-skip", Short::NoArg('X')),
("disable-fragmentation", Short::NoArg('F')),
("output-info", Short::Required('i')),
("never-skip", Short::Optional('z')),
("log-summary", Short::Optional('l')),
("log-debug", Short::Optional('L')),
];
if let Some((_, kind)) = table.iter().find(|(n, _)| *n == name) {
return Some(("", *kind));
}
let matches: Vec<&(&str, Short)> = table.iter().filter(|(n, _)| n.starts_with(name)).collect();
if matches.len() == 1 {
Some(("", matches[0].1))
} else {
None
}
}
fn apply(args: &mut Args, opt: Short, value: Option<String>) -> Result<(), String> {
let c = match opt {
Short::NoArg(c) | Short::Required(c) | Short::Optional(c) => c,
};
match c {
'A' => {
args.analyze = true;
args.query = true;
args.log_debug = Some(None);
args.verbose = true;
}
'v' => {
args.verbose = true;
args.quiet = false;
}
'q' => {
args.verbose = false;
args.quiet = true;
}
'e' => {
args.stderr_progress = true;
eprintln!("Sending all callbacks to stderr for wrapper script");
}
'l' => {
args.log_summary = Some(value);
}
'L' => {
args.log_debug = Some(value);
}
'V' => args.version = true,
'Q' => args.query = true,
's' => args.search = true,
'h' => args.help = true,
'p' => args.output_format = OutputFormat::RawHost,
'r' => args.output_format = OutputFormat::RawLittle,
'R' => args.output_format = OutputFormat::RawBig,
'w' => args.output_format = OutputFormat::Wav,
'f' => args.output_format = OutputFormat::Aiff,
'a' => args.output_format = OutputFormat::Aifc,
'c' => args.force_endian = Some(0),
'C' => args.force_endian = Some(1),
'n' => args.force_sectors = Some(atoi(value.as_deref().unwrap_or(""))),
'o' => args.force_overlap = Some(atoi(value.as_deref().unwrap_or(""))),
'd' => args.force_device = value,
'g' => {
if args.force_cooked_device.is_some() {
return Err("-g option incompatable with -k".to_string());
}
args.force_cooked_device = None;
args.force_generic_device = value;
}
'k' => {
if args.force_generic_device.is_some() || args.force_device.is_some() {
return Err("-k option incompatable with -d and -g".to_string());
}
args.force_cooked_device = value;
}
'S' => args.force_speed = Some(atoi(value.as_deref().unwrap_or(""))),
't' => args.toc_offset = atoi(value.as_deref().unwrap_or("")),
'T' => args.toc_bias = true,
'O' => args.sample_offset = atoi(value.as_deref().unwrap_or("")),
'z' => {
if let Some(v) = value {
args.max_retries = atoi(&v);
args.paranoia_mode &= !PARANOIA_MODE_NEVERSKIP;
} else {
args.paranoia_mode |= PARANOIA_MODE_NEVERSKIP;
}
}
'Z' => args.paranoia_mode = PARANOIA_MODE_DISABLE,
'Y' => {
args.paranoia_mode |= PARANOIA_MODE_OVERLAP;
args.paranoia_mode &= !PARANOIA_MODE_VERIFY;
}
'X' => args.abort_on_skip = true,
'W' => args.paranoia_mode &= !PARANOIA_MODE_REPAIR,
'F' => args.paranoia_mode &= !PARANOIA_MODE_FRAGMENT,
'B' => args.batch = true,
'i' => args.info_file = value,
_ => return Err(format!("Unknown option -{}", c)),
}
Ok(())
}
pub fn parse(argv: &[String]) -> Result<Args, String> {
let mut args = Args::default();
let mut positionals = Vec::new();
let mut i = 1usize;
let mut stop = false;
while i < argv.len() {
let arg = &argv[i];
if stop || arg == "-" || !arg.starts_with('-') {
positionals.push(arg.clone());
i += 1;
continue;
}
if arg == "--" {
stop = true;
i += 1;
continue;
}
if let Some(long) = arg.strip_prefix("--") {
let (name, inline) = match long.split_once('=') {
Some((n, v)) => (n, Some(v.to_string())),
None => (long, None),
};
let (_, kind) =
long_kind(name).ok_or_else(|| format!("unrecognized option '--{}'", name))?;
match kind {
Short::NoArg(c) => {
if inline.is_some() {
return Err(format!("option '--{}' doesn't allow an argument", name));
}
apply(&mut args, Short::NoArg(c), None)?;
if args.help || args.version {
return Ok(args);
}
}
Short::Required(c) => {
let value = match inline {
Some(v) => Some(v),
None => {
i += 1;
if i >= argv.len() {
return Err(format!("option '--{}' requires an argument", name));
}
Some(argv[i].clone())
}
};
apply(&mut args, Short::Required(c), value)?;
}
Short::Optional(c) => apply(&mut args, Short::Optional(c), inline)?,
}
i += 1;
continue;
}
let chars: Vec<char> = arg[1..].chars().collect();
let mut j = 0usize;
while j < chars.len() {
let c = chars[j];
let kind = short_kind(c).ok_or_else(|| format!("invalid option -- '{}'", c))?;
match kind {
Short::NoArg(c) => {
apply(&mut args, Short::NoArg(c), None)?;
if args.help || args.version {
return Ok(args);
}
j += 1;
}
Short::Required(c) => {
let rest: String = chars[j + 1..].iter().collect();
let value = if !rest.is_empty() {
rest
} else {
i += 1;
if i >= argv.len() {
return Err(format!("option requires an argument -- '{}'", c));
}
argv[i].clone()
};
apply(&mut args, Short::Required(c), Some(value))?;
break;
}
Short::Optional(c) => {
let rest: String = chars[j + 1..].iter().collect();
apply(
&mut args,
Short::Optional(c),
if rest.is_empty() { None } else { Some(rest) },
)?;
break;
}
}
}
i += 1;
}
if !positionals.is_empty() {
args.span = Some(positionals[0].clone());
}
if positionals.len() > 1 {
args.outfile = Some(positionals[1].clone());
}
Ok(args)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::OutputFormat;
fn p(args: &[&str]) -> Result<Args, String> {
let mut v = vec!["cdparanoia".to_string()];
v.extend(args.iter().map(|s| s.to_string()));
parse(&v)
}
#[test]
fn clustered_flags() {
let a = p(&["-vQ"]).unwrap();
assert!(a.verbose && !a.quiet && a.query);
let a = p(&["-qv"]).unwrap();
assert!(a.verbose && !a.quiet);
let a = p(&["-vq"]).unwrap();
assert!(a.quiet && !a.verbose);
}
#[test]
fn optional_arguments_are_attached_only() {
let a = p(&["-lfoo.log"]).unwrap();
assert_eq!(a.log_summary, Some(Some("foo.log".to_string())));
let a = p(&["-l", "foo.log"]).unwrap();
assert_eq!(a.log_summary, Some(None));
assert_eq!(a.span.as_deref(), Some("foo.log"));
let a = p(&["--log-summary=bar.log"]).unwrap();
assert_eq!(a.log_summary, Some(Some("bar.log".to_string())));
let a = p(&["--log-summary", "bar.log"]).unwrap();
assert_eq!(a.log_summary, Some(None));
}
#[test]
fn never_skip_modes() {
let a = p(&["-z", "1"]).unwrap();
assert_ne!(a.paranoia_mode & PARANOIA_MODE_NEVERSKIP, 0);
assert_eq!(a.span.as_deref(), Some("1"));
let a = p(&["-z5", "1"]).unwrap();
assert_eq!(a.paranoia_mode & PARANOIA_MODE_NEVERSKIP, 0);
assert_eq!(a.max_retries, 5);
}
#[test]
fn end_of_options() {
let a = p(&["--", "-3", "out.raw"]).unwrap();
assert_eq!(a.span.as_deref(), Some("-3"));
assert_eq!(a.outfile.as_deref(), Some("out.raw"));
}
#[test]
fn output_formats_last_wins() {
assert_eq!(
p(&["-w", "-r", "1"]).unwrap().output_format,
OutputFormat::RawLittle
);
assert_eq!(
p(&["-p", "-a", "1"]).unwrap().output_format,
OutputFormat::Aifc
);
}
#[test]
fn incompatible_devices() {
assert!(p(&["-g", "/dev/sg0", "-k", "/dev/sr0", "1"]).is_err());
}
}
+4
View File
@@ -0,0 +1,4 @@
fn main() {
let argv: Vec<String> = std::env::args().collect();
std::process::exit(cdrustanoia_cli::run(&argv));
}
+806
View File
@@ -0,0 +1,806 @@
//! cdrustanoia command line driver, ported from cdparanoia 10.2 main.c.
mod analyze;
pub mod args;
mod log;
mod output;
mod progress;
mod span;
use crate::log::Log;
use crate::output::{write_header, BufferedWriter, OutputFormat};
use crate::progress::Progress;
use crate::span::parse_span;
use args::Args;
use cdrustanoia_core::device::discover::{find_a_cdrom, identify, identify_cooked, identify_sgio};
use cdrustanoia_core::device::{host_big_endian, swap16, Drive};
use cdrustanoia_core::paranoia::*;
use cdrustanoia_core::{VERSION, VERSIONNUM};
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::FromRawFd;
use std::path::Path;
const MANUAL: &str = r#"cdparanoia III release 10.2 (September 11, 2008)
(C) 2008 Monty <monty@xiph.org> and Xiph.Org
Report bugs to paranoia@xiph.org
http://www.xiph.org/paranoia/
USAGE:
cdparanoia [options] <span> [outfile]
OPTIONS:
-A --analyze-drive : run and log a complete analysis of drive
caching, timing and reading behavior;
verifies that cdparanoia is correctly
modelling a sprcific drive's cache and
read behavior. Implies -vQL
-v --verbose : extra verbose operation
-q --quiet : quiet operation
-e --stderr-progress : force output of progress information to
stderr (for wrapper scripts)
-l --log-summary [<file>] : save result summary to file, default
filename cdparanoia.log
-L --log-debug [<file>] : save detailed device autosense and
debugging output to file, default
filename cdparanoia.log
-V --version : print version info and quit
-Q --query : autosense drive, query disc and quit
-B --batch : 'batch' mode (saves each track to a
seperate file.
-s --search-for-drive : do an exhaustive search for drive
-h --help : print help
-p --output-raw : output raw 16 bit PCM in host byte
order
-r --output-raw-little-endian : output raw 16 bit little-endian PCM
-R --output-raw-big-endian : output raw 16 bit big-endian PCM
-w --output-wav : output as WAV file (default)
-f --output-aiff : output as AIFF file
-a --output-aifc : output as AIFF-C file
-c --force-cdrom-little-endian : force treating drive as little endian
-C --force-cdrom-big-endian : force treating drive as big endian
-n --force-default-sectors <n> : force default number of sectors in read
to n sectors
-o --force-search-overlap <n> : force minimum overlap search during
verification to n sectors
-d --force-cdrom-device <dev> : use specified device; disallow
autosense
-k --force-cooked-device <dev> : use specified cdrom device and force
use of the old 'cooked ioctl' kernel
interface. -k cannot be used with -d
or -g.
-g --force-generic-device <dev> : use specified generic scsi device and
force use of the old SG kernel
interface. -g cannot be used with -k.
-S --force-read-speed <n> : read from device at specified speed; by
default, cdparanoia sets drive to full
speed.
-t --toc-offset <n> : Add <n> sectors to the values reported
when addressing tracks. May be negative
-T --toc-bias : Assume that the beginning offset of
track 1 as reported in the TOC will be
addressed as LBA 0. Necessary for some
Toshiba drives to get track boundaries
correct
-O --sample-offset <n> : Add <n> samples to the offset when
reading data. May be negative.
-z --never-skip[=n] : never accept any less than perfect
data reconstruction (don't allow 'V's)
but if [n] is given, skip after [n]
retries without progress.
-Z --disable-paranoia : disable all paranoia checking
-Y --disable-extra-paranoia : only do cdda2wav-style overlap checking
-X --abort-on-skip : abort on imperfect reads/skips
OUTPUT SMILIES:
:-) Normal operation, low/no jitter
:-| Normal operation, considerable jitter
:-/ Read drift
:-P Unreported loss of streaming in atomic read operation
8-| Finding read problems at same point during reread; hard to correct
:-0 SCSI/ATAPI transport error
:-( Scratch detected
;-( Gave up trying to perform a correction
8-X Aborted (as per -X) due to a scratch/skip
:^D Finished extracting
PROGRESS BAR SYMBOLS:
<space> No corrections needed
- Jitter correction required
+ Unreported loss of streaming/other error in read
! Errors are getting through stage 1 but corrected in stage2
e SCSI/ATAPI transport error (corrected)
V Uncorrected error/skip
SPAN ARGUMENT:
The span argument may be a simple track number or a offset/span
specification. The syntax of an offset/span takes the rough form:
1[ww:xx:yy.zz]-2[aa:bb:cc.dd]
Here, 1 and 2 are track numbers; the numbers in brackets provide a
finer grained offset within a particular track. [aa:bb:cc.dd] is in
hours/minutes/seconds/sectors format. Zero fields need not be
specified: [::20], [:20], [20], [20.], etc, would be interpreted as
twenty seconds, [10:] would be ten minutes, [.30] would be thirty
sectors (75 sectors per second).
When only a single offset is supplied, it is interpreted as a starting
offset and ripping will continue to the end of he track. If a single
offset is preceeded or followed by a hyphen, the implicit missing
offset is taken to be the start or end of the disc, respectively. Thus:
1:[20.35] Specifies ripping from track 1, second 20, sector 35 to
the end of track 1.
1:[20.35]- Specifies ripping from 1[20.35] to the end of the disc
-2 Specifies ripping from the beginning of the disc up to
(and including) track 2
-2:[30.35] Specifies ripping from the beginning of the disc up to
2:[30.35]
2-4 Specifies ripping from the beginning of track two to the
end of track 4.
Don't forget to protect square brackets and preceeding hyphens from
the shell...
A few examples, protected from the shell:
A) query only with exhaustive search for a drive and full reporting
of autosense:
cdparanoia -vsQ
B) extract up to and including track 3, putting each track in a seperate
file:
cdparanoia -B -- "-3"
C) extract from track 1, time 0:30.12 to 1:10.00:
cdparanoia "1[:30.12]-1[1:10]"
Submit bug reports to paranoia@xiph.org
"#;
fn usage(out: &mut dyn Write) {
let _ = out.write_all(MANUAL.as_bytes());
}
fn open_log_name(name: &str) -> Result<Box<dyn Write>, String> {
if name == "-" {
Ok(Box::new(io::stdout()))
} else {
OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.mode(0o666)
.open(name)
.map(|f| Box::new(f) as Box<dyn Write>)
.map_err(|e| format!("Cannot open log file {}: {}", name, e))
}
}
fn setup_logs(args: &Args, argv: &[String]) -> Result<Log, String> {
let mut log = Log::new();
log.quiet = args.quiet;
let summary_name = args
.log_summary
.as_ref()
.map(|v| v.clone().unwrap_or_else(|| "cdparanoia.log".to_string()));
let report_name = args
.log_debug
.as_ref()
.map(|v| v.clone().unwrap_or_else(|| "cdparanoia.log".to_string()));
match (&summary_name, &report_name) {
(Some(s), Some(r)) if s == r && s != "-" => {
let file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.mode(0o666)
.open(s)
.map_err(|e| format!("Cannot open log file {}: {}", s, e))?;
let clone = file
.try_clone()
.map_err(|e| format!("Cannot duplicate log file {}: {}", s, e))?;
log.set_summary_writer(Box::new(file));
log.set_report_writer(Box::new(clone));
}
_ => {
if let Some(name) = &summary_name {
log.set_summary_writer(open_log_name(name)?);
}
if let Some(name) = &report_name {
log.set_report_writer(open_log_name(name)?);
}
}
}
if log.summary_file.is_some() {
let line = argv.join(" ");
log.summary(format_args!("{}", line));
if summary_name.as_deref() != report_name.as_deref() {
log.summary(format_args!("{}\n", VERSION));
log.summary(format_args!("Using cdda library version: {}", VERSIONNUM));
log.summary(format_args!(
"Using paranoia library version: {}",
VERSIONNUM
));
}
}
if log.report_file.is_some() && summary_name.as_deref() != report_name.as_deref() {
let line = argv.join(" ");
log.report(format_args!("{}", line));
}
log.flush();
Ok(log)
}
fn display_toc(d: &dyn Drive, log: &mut Log) {
let mut s = String::new();
s.push_str("\nTable of contents (audio tracks only):\n");
s.push_str("track length begin copy pre ch\n");
s.push_str("===========================================================");
let mut audiolen = 0i64;
for i in 1..=d.tracks() {
if d.track_audiop(i) {
let sec = d.track_firstsector(i);
let off = d.track_lastsector(i) - sec + 1;
s.push_str(&format!(
"\n{:3}. {:7} [{:02}:{:02}.{:02}] {:7} [{:02}:{:02}.{:02}] {} {} {}",
i,
off,
off / (60 * 75),
(off / 75) % 60,
off % 75,
sec,
sec / (60 * 75),
(sec / 75) % 60,
sec % 75,
if d.track_copyp(i) { " OK" } else { " no" },
if d.track_preemp(i) { " yes" } else { " no" },
if d.track_channels(i) == 2 { " 2" } else { " 4" }
));
audiolen += off;
}
}
s.push_str(&format!(
"\nTOTAL {:7} [{:02}:{:02}.{:02}] (audio only)",
audiolen,
audiolen / (60 * 75),
(audiolen / 75) % 60,
audiolen % 75
));
log.report(format_args!("{}", s));
log.report(format_args!(" "));
}
fn read_sector(
p: &mut Paranoia,
progress: &mut Progress,
log: &mut Log,
max_retries: i32,
) -> Option<Vec<i16>> {
let mut callback = |pos: i64, func: i32| progress.callback(pos, func, log);
p.read_limited(&mut callback, max_retries)
.map(|s| s.to_vec())
}
fn samples_as_bytes(samples: &[i16]) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
samples.as_ptr() as *const u8,
std::mem::size_of_val(samples),
)
}
}
fn make_output_name(
template: &Option<String>,
batch: bool,
track: i32,
format: OutputFormat,
) -> Option<String> {
match template {
None => {
if batch {
Some(format!("track{:02}.{}", track, format.default_extension()))
} else {
Some(format.default_extension().to_string())
}
}
Some(name) if name == "-" => None,
Some(name) => {
let (prefix, file) = match name.rfind('/') {
Some(pos) => (&name[..pos + 1], &name[pos + 1..]),
None => ("", name.as_str()),
};
let mut out = if batch {
format!("{}track{:02}.{}", prefix, track, file)
} else {
format!("{}{}", prefix, file)
};
if file.is_empty() {
out.push_str(format.default_extension());
}
Some(out)
}
}
}
fn open_output(name: Option<&str>, template_stdout: bool) -> Result<File, String> {
if template_stdout {
let fd = unsafe { libc::dup(libc::STDOUT_FILENO) };
if fd < 0 {
return Err("Cannot duplicate stdout".to_string());
}
return Ok(unsafe { File::from_raw_fd(fd) });
}
let name = name.ok_or_else(|| "Invalid output file".to_string())?;
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.mode(0o666)
.open(name)
.map_err(|e| format!("Cannot open specified output file {}: {}", name, e))
}
pub fn run(argv: &[String]) -> i32 {
let args = match args::parse(argv) {
Ok(a) => a,
Err(e) => {
eprintln!("{}", e);
usage(&mut io::stderr());
return 1;
}
};
if args.help {
usage(&mut io::stdout());
return 0;
}
if args.version {
eprintln!("{}\n", VERSION);
return 0;
}
let mut log = match setup_logs(&args, argv) {
Ok(l) => l,
Err(e) => {
eprintln!("{}", e);
return 1;
}
};
let span_arg = args.span.clone();
if span_arg.is_none() && !args.query && !args.batch {
usage(&mut io::stderr());
let _ = io::stderr().flush();
return 1;
}
log.report(format_args!("{}\n", VERSION));
if args.verbose {
log.report(format_args!("Using cdda library version: {}", VERSIONNUM));
log.report(format_args!(
"Using paranoia library version: {}",
VERSIONNUM
));
}
let drive_result: Result<Box<dyn Drive>, String> = if let Some(dev) = &args.force_cooked_device
{
identify_cooked(dev, args.verbose)
} else if let Some(dev) = &args.force_generic_device {
match identify_sgio(dev, args.verbose) {
Ok(d) => Ok(d),
Err(e) => {
if let Some(spec) = &args.force_device {
identify(spec, args.verbose, false).map_err(|_| e)
} else {
Err(e)
}
}
}
} else if let Some(dev) = &args.force_device {
identify(dev, args.verbose, false)
} else if args.search {
find_a_cdrom(args.verbose).ok_or_else(|| "No cdrom drives found".to_string())
} else if Path::new("/dev/cdrom").symlink_metadata().is_err() {
find_a_cdrom(args.verbose).ok_or_else(|| "No cdrom drives found".to_string())
} else {
match identify("/dev/cdrom", args.verbose, false) {
Ok(d) => Ok(d),
Err(_) => {
if !args.verbose {
log.report_c(format_args!(
"\n/dev/cdrom exists but isn't accessible. By default,\n\
cdparanoia stops searching for an accessible drive here.\n\
Consider using -sv to force a more complete autosense\n\
of the machine.\n\nMore information about /dev/cdrom:"
));
let _ = identify("/dev/cdrom", true, false);
log.report(format_args!("\n"));
return 1;
}
Err("Unable to open cdrom drive".to_string())
}
}
};
let mut drive = match drive_result {
Ok(d) => d,
Err(_) => {
if !args.verbose {
log.report(format_args!(
"\nUnable to open cdrom drive; -v will give more information."
));
}
return 1;
}
};
if let Err(e) = drive.open_disc() {
if args.verbose {
log.report(format_args!("\nDisc open failed: {}", e));
}
log.report(format_args!(
"\nUnable to open disc. Is there an audio CD in the drive?"
));
return 1;
}
if !drive.opened() || drive.tracks() < 1 {
log.report(format_args!(
"\nUnable to open disc. Is there an audio CD in the drive?"
));
return 1;
}
if let Some(endian) = args.force_endian {
drive.set_bigendianp(endian);
log.report(format_args!(
"Forcing CDROM sense to {}-endian; ignoring preset and autosense",
if endian == 0 { "little" } else { "big" }
));
}
if let Some(sectors) = args.force_sectors {
if !(1..=100).contains(&sectors) {
log.report(format_args!(
"Default sector read size must be 1<= n <= 100"
));
return 1;
}
log.report(format_args!(
"Forcing default to read {} sectors; ignoring preset and autosense",
sectors
));
drive.set_nsectors(sectors as i64);
}
if let Some(overlap) = args.force_overlap {
if !(0..=75).contains(&overlap) {
log.report(format_args!("Search overlap sectors must be 0<= n <=75"));
return 1;
}
log.report(format_args!(
"Forcing search overlap to {} sectors; ignoring autosense",
overlap
));
}
let speed = match args.force_speed {
Some(0) | None => -1,
Some(v) => v,
};
if speed != -1 {
log.report(format_args!("\nAttempting to set speed to {}x... ", speed));
} else if args.verbose {
log.report(format_args!("\nAttempting to set cdrom to full speed... "));
}
if drive.speed_set(speed) != 0 {
if args.verbose || speed != -1 {
log.report(format_args!(
"\tCDROM speed set FAILED. Continuing anyway..."
));
}
} else if args.verbose {
log.report(format_args!("\tdrive returned OK."));
}
if args.analyze {
let warn = analyze::analyze_cache(drive.as_mut(), &mut log, speed);
if warn == 0 {
log.report_c(format_args!("\nDrive tests OK with Paranoia.\n\n"));
return 0;
}
log.report_c(format_args!(
"\nWARNING! PARANOIA MAY NOT BE TRUSTWORTHY WITH THIS DRIVE!\n\
\nThe Paranoia library may not model this CDROM drive's cache\
\ncorrectly according to this analysis run. Analysis is not\
\nalways accurate (it can be fooled by machine load or random\
\nkernel latencies), but if a failed result happens more often\
\nthan one time in twenty on an unloaded machine, please mail\
\nthe cdparanoia.log file produced by this failed analysis to\
\nparanoia-dev@xiph.org to assist developers in extending\
\nParanoia to handle this CDROM properly.\n\n"
));
return 1;
}
if args.query || args.verbose {
display_toc(drive.as_ref(), &mut log);
}
if args.query {
return 0;
}
let mut toc_offset = args.toc_offset as i64;
let mut sample_offset = args.sample_offset;
if sample_offset != 0 {
toc_offset += (sample_offset / 588) as i64;
sample_offset %= 588;
if sample_offset < 0 {
sample_offset += 588;
toc_offset -= 1;
}
}
if args.toc_bias {
toc_offset = -drive.track_firstsector(1);
}
{
let tracks = drive.tracks();
let toc = drive.toc_mut();
for i in 0..=tracks as usize {
toc.entries[i].start_sector += toc_offset;
}
}
if drive.nsectors() == 1 {
log.report(format_args!(
"WARNING: The autosensed/selected sectors per read value is\n\
one sector, making it very unlikely Paranoia can \n\
work.\n\n\
Attempting to continue...\n"
));
}
let (first_sector, last_sector) = match &span_arg {
Some(span) => match parse_span(drive.toc(), span) {
Ok(v) => v,
Err(e) => {
log.report(format_args!("{}", e));
return 1;
}
},
None => (drive.disc_firstsector(), drive.disc_lastsector()),
};
let track1 = drive.sector_gettrack(first_sector);
let track2 = drive.sector_gettrack(last_sector);
if track1 < 0 || track2 < 0 {
log.report(format_args!("Error parsing span argument"));
return 1;
}
for i in track1..=track2 {
if !drive.track_audiop(i) {
log.report(format_args!(
"Selected span contains non audio tracks. Aborting.\n"
));
return 1;
}
}
let off1 = first_sector - drive.track_firstsector(track1);
let off2 = last_sector - drive.track_firstsector(track2);
log.report(format_args!(
"Ripping from sector {:7} (track {:2} [{}:{:02}.{:02}])\n\t to sector {:7} (track {:2} [{}:{:02}.{:02}])",
first_sector,
track1,
off1 / (60 * 75),
(off1 / 75) % 60,
off1 % 75,
last_sector,
track2,
off2 / (60 * 75),
(off2 / 75) % 60,
off2 % 75
));
let mut p = Paranoia::new(drive);
p.modeset(args.paranoia_mode);
if let Some(overlap) = args.force_overlap {
p.overlapset(overlap as i64);
}
p.seek(first_sector, 0);
unsafe {
libc::seteuid(libc::getuid());
libc::setegid(libc::getgid());
}
if sample_offset != 0 {
let tracks = p.drive().tracks() as usize;
if let Some(e) = p.drive_mut().toc_mut().entries.get_mut(tracks) {
e.start_sector += 1;
}
}
let host_big = host_big_endian();
let target_big = args.output_format.output_big_endian(host_big);
let mut offset_buffer = [0u8; 2352];
let mut offset_buffer_used = 0usize;
let mut offset_skip = (sample_offset * 4) as usize;
let mut cursor = first_sector;
let mut progress = Progress::default();
progress.callscript = args.stderr_progress;
progress.abort_on_skip = args.abort_on_skip;
while cursor <= last_sector {
let (batch_first, batch_last, batch_track) = if args.batch {
let bt = p.drive().sector_gettrack(cursor);
if bt < 0 {
break;
}
let mut bl = p.drive().track_lastsector(bt);
if bl > last_sector {
bl = last_sector;
}
(cursor, bl, bt)
} else {
(first_sector, last_sector, -1)
};
progress.callbegin = batch_first;
progress.callend = batch_last;
progress.reset_batch();
let template_stdout = args.outfile.as_deref() == Some("-");
let outfile_name =
make_output_name(&args.outfile, args.batch, batch_track, args.output_format);
if template_stdout {
if args.batch {
log.report(format_args!(
"Are you sure you wanted 'batch' (-B) output with stdout?"
));
}
log.report(format_args!("outputting to stdout"));
} else {
log.report(format_args!(
"outputting to {}",
outfile_name.as_deref().unwrap_or("")
));
}
if let Some(name) = &outfile_name {
log.summary(format_args!("outputting to {}", name));
} else {
log.summary(format_args!("outputting to stdout"));
}
let file = match open_output(outfile_name.as_deref(), template_stdout) {
Ok(f) => f,
Err(e) => {
log.report(format_args!("{}", e));
return 1;
}
};
let mut out = BufferedWriter::new(file);
let bytes = ((batch_last - batch_first + 1) * 2352) as u32;
if let Err(e) = write_header(&mut out, args.output_format, bytes) {
log.report(format_args!("Error writing output: {}", e));
return 1;
}
if offset_buffer_used > 0 {
cursor += 1;
if let Err(e) = out.write_all(&offset_buffer[offset_buffer_used..]) {
log.report(format_args!("Error writing output: {}", e));
return 1;
}
}
progress.skipped_flag = false;
while cursor <= batch_last {
let sector = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) {
Some(s) => s,
None => {
progress.skipped_flag = true;
log.report(format_args!(
"\nparanoia_read: Unrecoverable error, bailing."
));
break;
}
};
if progress.skipped_flag && args.abort_on_skip {
cursor = batch_last + 1;
break;
}
progress.skipped_flag = false;
cursor += 1;
let mut data = sector;
if target_big != host_big {
for s in &mut data {
*s = swap16(*s);
}
}
progress.callback(cursor * 1176 - 1, -2, &mut log);
let data_bytes = samples_as_bytes(&data);
if let Err(e) = out.write_all(&data_bytes[offset_skip.min(data_bytes.len())..]) {
log.report(format_args!("Error writing output: {}", e));
return 1;
}
offset_skip = 0;
if sample_offset != 0 && cursor > batch_last {
let extra = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) {
Some(s) => s,
None => {
progress.skipped_flag = true;
log.report(format_args!(
"\nparanoia_read: Unrecoverable error reading through \n\
\t\t\tsample_offset shift\n\tat end of track, bailing."
));
break;
}
};
if progress.skipped_flag && args.abort_on_skip {
break;
}
progress.skipped_flag = false;
let mut extra = extra;
if target_big != host_big {
for s in &mut extra {
*s = swap16(*s);
}
}
let eb = samples_as_bytes(&extra);
offset_buffer[..eb.len().min(2352)].copy_from_slice(&eb[..eb.len().min(2352)]);
offset_buffer_used = (sample_offset as usize * 4).min(2352);
progress.callback(cursor * 1176, -2, &mut log);
if let Err(e) = out.write_all(&offset_buffer[..offset_buffer_used]) {
log.report(format_args!("Error writing output: {}", e));
return 1;
}
}
}
progress.callback(cursor * 1176 - 1, -1, &mut log);
if let Err(e) = out.close() {
log.report(format_args!("Error writing output: {}", e));
return 1;
}
if progress.skipped_flag {
if let Some(name) = &outfile_name {
log.report(format_args!("\nRemoving aborted file: {}", name));
let _ = std::fs::remove_file(name);
}
if batch_track != -1 {
let next_track = batch_track + 1;
let next = p.drive().track_firstsector(next_track);
if next >= 0 {
cursor = next;
p.seek(cursor, 0);
} else {
cursor = last_sector + 1;
}
offset_skip = (sample_offset * 4) as usize;
offset_buffer_used = 0;
}
}
log.report(format_args!("\n"));
}
log.report(format_args!("Done.\n\n"));
log.flush();
0
}
+121
View File
@@ -0,0 +1,121 @@
//! Report/log destination handling, matching report.h's macros plus the
//! separate summary and debug streams from main.c.
use std::io::{self, Write};
pub struct Log {
pub quiet: bool,
pub summary_file: Option<Box<dyn Write>>,
pub report_file: Option<Box<dyn Write>>,
}
impl Default for Log {
fn default() -> Self {
Self::new()
}
}
impl Log {
pub fn new() -> Self {
Self {
quiet: false,
summary_file: None,
report_file: None,
}
}
pub fn set_summary_writer(&mut self, writer: Box<dyn Write>) {
self.summary_file = Some(writer);
}
pub fn set_report_writer(&mut self, writer: Box<dyn Write>) {
self.report_file = Some(writer);
}
pub fn report(&mut self, args: std::fmt::Arguments<'_>) {
if !self.quiet {
let _ = writeln!(io::stderr(), "{}", args);
}
if let Some(f) = self.report_file.as_mut() {
let _ = writeln!(f, "{}", args);
}
}
pub fn report_c(&mut self, args: std::fmt::Arguments<'_>) {
if !self.quiet {
let _ = write!(io::stderr(), "{}", args);
}
if let Some(f) = self.report_file.as_mut() {
let _ = write!(f, "{}", args);
}
}
pub fn print_c(&mut self, args: std::fmt::Arguments<'_>) {
if !self.quiet {
let _ = write!(io::stderr(), "{}", args);
}
}
pub fn log_c(&mut self, args: std::fmt::Arguments<'_>) {
if let Some(f) = self.report_file.as_mut() {
let _ = write!(f, "{}", args);
}
}
pub fn summary(&mut self, args: std::fmt::Arguments<'_>) {
if let Some(f) = self.summary_file.as_mut() {
let _ = writeln!(f, "{}", args);
}
}
pub fn write_summary_bytes(&mut self, data: &[u8]) {
if let Some(f) = self.summary_file.as_mut() {
let _ = f.write_all(data);
let _ = f.flush();
}
}
pub fn flush(&mut self) {
if let Some(f) = self.summary_file.as_mut() {
let _ = f.flush();
}
if let Some(f) = self.report_file.as_mut() {
let _ = f.flush();
}
}
}
#[macro_export]
macro_rules! report {
($log:expr, $($arg:tt)*) => {
$log.report(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! report_c {
($log:expr, $($arg:tt)*) => {
$log.report_c(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! print_c {
($log:expr, $($arg:tt)*) => {
$log.print_c(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! log_c {
($log:expr, $($arg:tt)*) => {
$log.log_c(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! summary {
($log:expr, $($arg:tt)*) => {
$log.summary(format_args!($($arg)*))
};
}
+4
View File
@@ -0,0 +1,4 @@
fn main() {
let argv: Vec<String> = std::env::args().collect();
std::process::exit(cdrustanoia_cli::run(&argv));
}
+226
View File
@@ -0,0 +1,226 @@
//! WAV/AIFF/AIFC headers and buffered output, ported from header.c and
//! buffering_write.c.
use std::fs::File;
use std::io::{self, Write};
pub const OUTBUF_SZ: usize = 32 * 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OutputFormat {
RawHost,
RawLittle,
RawBig,
Wav,
Aiff,
Aifc,
}
impl OutputFormat {
pub fn default_extension(self) -> &'static str {
match self {
OutputFormat::RawHost | OutputFormat::RawLittle | OutputFormat::RawBig => "cdda.raw",
OutputFormat::Wav => "cdda.wav",
OutputFormat::Aifc => "cdda.aifc",
OutputFormat::Aiff => "cdda.aiff",
}
}
pub fn track_extension(self) -> &'static str {
match self {
OutputFormat::RawHost | OutputFormat::RawLittle | OutputFormat::RawBig => "raw",
OutputFormat::Wav => "wav",
OutputFormat::Aifc => "aifc",
OutputFormat::Aiff => "aiff",
}
}
pub fn output_big_endian(self, host_big: bool) -> bool {
match self {
OutputFormat::RawHost => host_big,
OutputFormat::RawLittle | OutputFormat::Wav => false,
OutputFormat::RawBig | OutputFormat::Aiff | OutputFormat::Aifc => true,
}
}
}
fn put_num<W: Write>(w: &mut W, num: u32, big_endian: bool, bytes: usize) -> io::Result<()> {
let mut tmp = [0u8; 4];
if big_endian {
let b = num.to_be_bytes();
for (i, slot) in tmp.iter_mut().take(bytes).enumerate() {
*slot = b[4 - bytes + i];
}
} else {
let b = num.to_le_bytes();
tmp[..bytes].copy_from_slice(&b[..bytes]);
}
w.write_all(&tmp[..bytes])
}
pub fn write_wav<W: Write>(w: &mut W, bytes: u32) -> io::Result<()> {
w.write_all(b"RIFF")?;
put_num(w, bytes.wrapping_add(44).wrapping_sub(8), false, 4)?;
w.write_all(b"WAVEfmt ")?;
put_num(w, 16, false, 4)?;
put_num(w, 1, false, 2)?;
put_num(w, 2, false, 2)?;
put_num(w, 44100, false, 4)?;
put_num(w, 44100 * 2 * 2, false, 4)?;
put_num(w, 4, false, 2)?;
put_num(w, 16, false, 2)?;
w.write_all(b"data")?;
put_num(w, bytes, false, 4)?;
Ok(())
}
pub fn write_aiff<W: Write>(w: &mut W, bytes: u32) -> io::Result<()> {
let frames = bytes / 4;
w.write_all(b"FORM")?;
put_num(w, bytes.wrapping_add(54).wrapping_sub(8), true, 4)?;
w.write_all(b"AIFF")?;
w.write_all(b"COMM")?;
put_num(w, 18, true, 4)?;
put_num(w, 2, true, 2)?;
put_num(w, frames, true, 4)?;
put_num(w, 16, true, 2)?;
w.write_all(&[0x40, 0x0e, 0xac, 0x44, 0, 0, 0, 0, 0, 0])?;
w.write_all(b"SSND")?;
put_num(w, bytes.wrapping_add(8), true, 4)?;
put_num(w, 0, true, 4)?;
put_num(w, 0, true, 4)?;
Ok(())
}
pub fn write_aifc<W: Write>(w: &mut W, bytes: u32) -> io::Result<()> {
let frames = bytes / 4;
w.write_all(b"FORM")?;
put_num(w, bytes.wrapping_add(86).wrapping_sub(8), true, 4)?;
w.write_all(b"AIFC")?;
w.write_all(b"FVER")?;
put_num(w, 4, true, 4)?;
put_num(w, 2726318400, true, 4)?;
w.write_all(b"COMM")?;
put_num(w, 38, true, 4)?;
put_num(w, 2, true, 2)?;
put_num(w, frames, true, 4)?;
put_num(w, 16, true, 2)?;
w.write_all(&[0x40, 0x0e, 0xac, 0x44, 0, 0, 0, 0, 0, 0])?;
w.write_all(b"NONE")?;
put_num(w, 14, true, 1)?;
w.write_all(b"not compressed")?;
put_num(w, 0, true, 1)?;
w.write_all(b"SSND")?;
put_num(w, bytes.wrapping_add(8), true, 4)?;
put_num(w, 0, true, 4)?;
put_num(w, 0, true, 4)?;
Ok(())
}
pub fn write_header<W: Write>(w: &mut W, format: OutputFormat, bytes: u32) -> io::Result<()> {
match format {
OutputFormat::Wav => write_wav(w, bytes),
OutputFormat::Aiff => write_aiff(w, bytes),
OutputFormat::Aifc => write_aifc(w, bytes),
_ => Ok(()),
}
}
/// Buffered writer matching the original's 32 KiB write coalescing.
pub struct BufferedWriter {
file: File,
buf: Vec<u8>,
closed: bool,
}
impl BufferedWriter {
pub fn new(file: File) -> Self {
Self {
file,
buf: Vec::with_capacity(OUTBUF_SZ),
closed: false,
}
}
pub fn write_all(&mut self, mut data: &[u8]) -> io::Result<()> {
while !data.is_empty() {
let space = OUTBUF_SZ.saturating_sub(self.buf.len());
if space == 0 {
self.flush_buf()?;
continue;
}
let n = space.min(data.len());
self.buf.extend_from_slice(&data[..n]);
data = &data[n..];
}
Ok(())
}
fn flush_buf(&mut self) -> io::Result<()> {
if !self.buf.is_empty() {
self.file.write_all(&self.buf)?;
self.buf.clear();
}
Ok(())
}
pub fn flush(&mut self) -> io::Result<()> {
self.flush_buf()?;
self.file.flush()
}
pub fn close(mut self) -> io::Result<()> {
self.closed = true;
self.flush()
}
}
impl Write for BufferedWriter {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
self.write_all(data)?;
Ok(data.len())
}
fn flush(&mut self) -> io::Result<()> {
BufferedWriter::flush(self)
}
}
impl Drop for BufferedWriter {
fn drop(&mut self) {
if !self.closed {
let _ = self.flush_buf();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wav_header_size() {
let mut v = Vec::new();
write_wav(&mut v, 2352).unwrap();
assert_eq!(v.len(), 44);
assert_eq!(&v[..4], b"RIFF");
assert_eq!(&v[8..16], b"WAVEfmt ");
assert_eq!(&v[36..40], b"data");
}
#[test]
fn aiff_header_size() {
let mut v = Vec::new();
write_aiff(&mut v, 2352).unwrap();
assert_eq!(v.len(), 54);
assert_eq!(&v[..4], b"FORM");
assert_eq!(&v[8..12], b"AIFF");
}
#[test]
fn aifc_header_size() {
let mut v = Vec::new();
write_aifc(&mut v, 2352).unwrap();
assert_eq!(v.len(), 86);
assert_eq!(&v[8..12], b"AIFC");
}
}
+351
View File
@@ -0,0 +1,351 @@
//! Callback and progress display, ported from the callback in main.c.
use crate::log::Log;
use cdrustanoia_core::callback::*;
use cdrustanoia_core::device::CD_FRAMEWORDS;
use std::io::{self, Write};
use std::time::{SystemTime, UNIX_EPOCH};
pub const CALLBACK_STRINGS: [&str; 16] = [
"wrote",
"finished",
"read",
"verify",
"jitter",
"correction",
"scratch",
"scratch repair",
"skip",
"drift",
"backoff",
"overlap",
"dropped",
"duped",
"transport error",
"cache error",
];
const GRAPH: usize = 30;
pub struct Progress {
pub callbegin: i64,
pub callend: i64,
pub callscript: bool,
pub abort_on_skip: bool,
pub skipped_flag: bool,
c_sector: i64,
v_sector: i64,
dispcache: [u8; GRAPH],
last: i32,
lasttime: i64,
heartbeat: u8,
slevel: i32,
slast: i32,
stimeout: i32,
cacheerr: i32,
overlap: i32,
printit: i32,
}
impl Default for Progress {
fn default() -> Self {
Self {
callbegin: 0,
callend: 0,
callscript: false,
abort_on_skip: false,
skipped_flag: false,
c_sector: 0,
v_sector: 0,
dispcache: [b' '; GRAPH],
last: 0,
lasttime: 0,
heartbeat: b' ',
slevel: 0,
slast: 0,
stimeout: 0,
cacheerr: 0,
overlap: 0,
printit: -1,
}
}
}
impl Progress {
pub fn reset_batch(&mut self) {
self.c_sector = 0;
self.v_sector = 0;
self.dispcache = [b' '; GRAPH];
self.last = 0;
self.lasttime = 0;
self.heartbeat = b' ';
self.slevel = 0;
self.slast = 0;
self.stimeout = 0;
self.overlap = 0;
self.skipped_flag = false;
}
fn now_tenths() -> i64 {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs() as i64 * 10 + (d.subsec_micros() / 100_000) as i64,
Err(_) => 0,
}
}
pub fn callback(&mut self, inpos: i64, function: i32, log: &mut Log) {
if self.callscript {
let name = if (-2..=13).contains(&function) {
CALLBACK_STRINGS[(function + 2) as usize]
} else {
""
};
let _ = writeln!(io::stderr(), "##: {} [{}] @ {}", function, name, inpos);
} else if function == PARANOIA_CB_CACHEERR {
if self.cacheerr == 0 {
let _ = write!(
io::stderr(),
"\rWARNING: The CDROM drive appears to be seeking impossibly quickly.\n\
This could be due to timer bugs, a drive that really is improbably fast,\n\
or, most likely, a bug in cdparanoia's cache modelling.\n\n\
Please consider using the -A option to perform an analysis run, then mail\n\
the cdparanoia.log file produced by the analysis to paranoia-dev@xiph.org\n\
to assist developers in correcting the problem.\n\n"
);
}
self.cacheerr += 1;
}
if function == PARANOIA_CB_SKIP {
// Keep -X functional even when progress is redirected or quiet.
self.skipped_flag = true;
}
if log.quiet {
return;
}
let osector = inpos;
let sector = inpos / CD_FRAMEWORDS;
if self.printit == -1 {
self.printit = if unsafe { libc::isatty(2) } == 1 {
1
} else {
0
};
}
if self.printit == 1 {
let position = if self.callend != self.callbegin {
((sector - self.callbegin) as f64 / (self.callend - self.callbegin) as f64
* GRAPH as f64) as i32
} else {
0
};
let aheadposition = if self.callend != self.callbegin {
((self.c_sector - self.callbegin) as f64 / (self.callend - self.callbegin) as f64
* GRAPH as f64) as i32
} else {
0
};
if function == -2 {
self.v_sector = sector;
return;
}
if function == -1 {
self.last = 8;
self.heartbeat = b'*';
self.slevel = 0;
self.v_sector = sector;
} else if position < GRAPH as i32 && position >= 0 {
let p = position as usize;
match function {
PARANOIA_CB_VERIFY => {
if self.stimeout >= 30 {
if self.overlap as i64 > CD_FRAMEWORDS {
self.slevel = 2;
} else {
self.slevel = 1;
}
}
}
PARANOIA_CB_READ => {
if sector > self.c_sector {
self.c_sector = sector;
}
}
PARANOIA_CB_FIXUP_EDGE => {
if self.stimeout >= 5 {
if self.overlap as i64 > CD_FRAMEWORDS {
self.slevel = 2;
} else {
self.slevel = 1;
}
}
if self.dispcache[p] == b' ' {
self.dispcache[p] = b'-';
}
}
PARANOIA_CB_FIXUP_ATOM => {
if self.slevel < 3 || self.stimeout > 5 {
self.slevel = 3;
}
if self.dispcache[p] == b' ' || self.dispcache[p] == b'-' {
self.dispcache[p] = b'+';
}
}
PARANOIA_CB_READERR => {
self.slevel = 6;
if self.dispcache[p] != b'V' && self.dispcache[p] != b'C' {
self.dispcache[p] = b'e';
}
}
PARANOIA_CB_CACHEERR => {
self.slevel = 8;
self.dispcache[p] = b'C';
}
PARANOIA_CB_SKIP => {
self.slevel = 8;
if self.dispcache[p] != b'C' {
self.dispcache[p] = b'V';
}
}
PARANOIA_CB_OVERLAP => {
self.overlap = osector as i32;
}
PARANOIA_CB_SCRATCH => {
self.slevel = 7;
}
PARANOIA_CB_DRIFT => {
if self.slevel < 4 || self.stimeout > 5 {
self.slevel = 4;
}
}
PARANOIA_CB_FIXUP_DROPPED | PARANOIA_CB_FIXUP_DUPED => {
self.slevel = 5;
if matches!(self.dispcache[p], b' ' | b'-' | b'+') {
self.dispcache[p] = b'!';
}
}
_ => {}
}
}
let mut smilie = "= :-)";
match self.slevel {
0 => {
smilie = if self.skipped_flag { " 8-X" } else { " :^D" };
}
1 => smilie = " :-)",
2 => smilie = " :-|",
3 => smilie = " :-P",
4 => smilie = " :-/",
5 => smilie = " 8-|",
6 => smilie = " :-0",
7 => smilie = " :-(",
8 => {
smilie = " ;-(";
self.skipped_flag = true;
}
_ => {}
}
let test = Self::now_tenths();
if self.lasttime != test || function == -1 || self.slast != self.slevel {
if self.lasttime != test || function == -1 {
self.last += 1;
self.lasttime = test;
if self.last > 7 {
self.last = 0;
}
self.stimeout += 1;
self.heartbeat = match self.last {
0 => b' ',
1 | 7 => b'.',
2 | 6 => b'o',
3 | 5 => b'0',
4 => b'O',
_ => self.heartbeat,
};
if function == -1 {
self.heartbeat = b'*';
}
}
if self.slast != self.slevel {
self.stimeout = 0;
}
self.slast = self.slevel;
let mut buffer: Vec<u8> =
if self.abort_on_skip && self.skipped_flag && function != -1 {
format!(
"\r (== PROGRESS == [{}| {:06} {:02} ] =={} {} ==) ",
" ...aborting; please wait... ",
self.v_sector,
self.overlap as i64 / CD_FRAMEWORDS,
smilie,
self.heartbeat as char
)
.into_bytes()
} else {
let disp = String::from_utf8_lossy(&self.dispcache).to_string();
if self.v_sector == 0 {
format!(
"\r (== PROGRESS == [{}| ...... {:02} ] =={} {} ==) ",
disp,
self.overlap as i64 / CD_FRAMEWORDS,
smilie,
self.heartbeat as char
)
.into_bytes()
} else {
format!(
"\r (== PROGRESS == [{}| {:06} {:02} ] =={} {} ==) ",
disp,
self.v_sector,
self.overlap as i64 / CD_FRAMEWORDS,
smilie,
self.heartbeat as char
)
.into_bytes()
}
};
if aheadposition >= 0
&& aheadposition < GRAPH as i32
&& function != -1
&& buffer.len() > 19 + aheadposition as usize
{
buffer[19 + aheadposition as usize] = b'>';
}
let _ = io::stderr().write_all(&buffer);
if function == -1 {
let mut report_bytes = buffer[1..].to_vec();
report_bytes.extend_from_slice(b"\n\n");
log.write_summary_bytes(&report_bytes);
}
}
}
if function == -1 {
self.dispcache = [b' '; GRAPH];
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skip_flag_survives_redirected_output() {
let mut p = Progress::default();
let mut log = Log::new();
log.quiet = true;
p.callback(0, PARANOIA_CB_SKIP, &mut log);
assert!(p.skipped_flag);
}
}
+271
View File
@@ -0,0 +1,271 @@
//! Span argument parsing, a direct port of `parse_offset()` from main.c.
use cdrustanoia_core::device::DiscToc;
#[derive(Debug, Clone)]
pub struct SpanError(pub String);
impl std::fmt::Display for SpanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for SpanError {}
fn err(msg: impl Into<String>) -> SpanError {
SpanError(msg.into())
}
#[derive(Default, Debug, Clone, Copy)]
struct TimeParts {
hours: Option<i64>,
minutes: Option<i64>,
seconds: Option<i64>,
sectors: Option<i64>,
}
fn parse_num(s: &str, original: &str) -> Result<i64, SpanError> {
let digits: String = s.chars().take_while(|c| c.is_ascii_digit()).collect();
if digits.is_empty() {
Ok(0)
} else {
digits
.parse::<i64>()
.map_err(|_| err(format!("Error parsing span argument: {original}")))
}
}
fn parse_time(time: &str) -> Result<TimeParts, SpanError> {
let mut parts = TimeParts::default();
let mut rest = time;
if let Some(pos) = rest.rfind('.') {
parts.sectors = Some(parse_num(&rest[pos + 1..], time)?);
rest = &rest[..pos];
if rest.contains('.') {
return Err(err("Error parsing span argument"));
}
}
let fields: Vec<&str> = rest.split(':').collect();
if fields.len() > 3 {
return Err(err("Error parsing span argument"));
}
// The original walks right-to-left: rightmost field is seconds, then
// minutes, then hours.
match fields.len() {
1 => {
parts.seconds = Some(parse_num(fields[0], time)?);
}
2 => {
parts.seconds = Some(parse_num(fields[1], time)?);
parts.minutes = Some(parse_num(fields[0], time)?);
}
3 => {
parts.seconds = Some(parse_num(fields[2], time)?);
parts.minutes = Some(parse_num(fields[1], time)?);
parts.hours = Some(parse_num(fields[0], time)?);
}
_ => return Err(err("Error parsing span argument")),
}
Ok(parts)
}
fn has_time(parts: &TimeParts) -> bool {
parts.seconds.is_some() || parts.sectors.is_some()
}
/// Port of `parse_offset`. `begin` is the beginning of a span, if known.
pub fn parse_offset(
toc: &DiscToc,
offset: Option<&str>,
begin: Option<i64>,
) -> Result<i64, SpanError> {
let offset = match offset {
Some(s) => s,
None => return Ok(-1),
};
let (track_text, time_text) = if let Some(close) = offset.find(']') {
let before = &offset[..close];
if let Some(open) = before.find('[') {
(&before[..open], Some(&before[open + 1..]))
} else {
return Err(err("Error parsing span argument"));
}
} else {
(offset, None)
};
let digit_count = track_text
.chars()
.take_while(|c| c.is_ascii_digit())
.count();
let track = if digit_count > 0 {
let t = track_text[..digit_count]
.parse::<i64>()
.map_err(|_| err("Error parsing span argument"))?;
if t < 0 || t > toc.tracks as i64 {
return Err(err(format!("Track #{t} does not exist.")));
}
Some(t)
} else {
None
};
let parts = match time_text {
Some(t) if !t.is_empty() => parse_time(t)?,
Some(_) => TimeParts::default(),
None => TimeParts::default(),
};
let ret = match track {
None => {
if !has_time(&parts) {
return Ok(-1);
}
begin.unwrap_or_else(|| toc.disc_firstsector())
}
Some(t) => {
if !has_time(&parts) {
if begin.is_none() {
return Ok(toc.track_firstsector(t as i32));
}
return Ok(toc.track_lastsector(t as i32));
}
toc.track_firstsector(t as i32)
}
};
let mut ret = ret;
let mut add = |v: i64, scale: i64| -> Result<(), SpanError> {
ret = ret
.checked_add(
v.checked_mul(scale)
.ok_or_else(|| err("Error parsing span argument"))?,
)
.ok_or_else(|| err("Error parsing span argument"))?;
Ok(())
};
if let Some(v) = parts.sectors {
add(v, 1)?;
}
if let Some(v) = parts.seconds {
add(v, 75)?;
}
if let Some(v) = parts.minutes {
add(v, 60 * 75)?;
}
if let Some(v) = parts.hours {
add(v, 60 * 60 * 75)?;
}
if let Some(t) = track {
if toc.sector_gettrack(ret) != t as i32 {
return Err(err(
"Time/sector offset goes beyond end of specified track.",
));
}
}
if ret > toc.disc_lastsector() {
return Err(err("Time/sector offset goes beyond end of disc."));
}
Ok(ret)
}
/// Parse a complete span expression, returning inclusive first/last sectors.
pub fn parse_span(toc: &DiscToc, span: &str) -> Result<(i64, i64), SpanError> {
let first_hyphen = span.find('-');
let last_hyphen = span.rfind('-');
if first_hyphen != last_hyphen {
return Err(err("Error parsing span argument"));
}
let (part1, part2, had_hyphen) = match first_hyphen {
Some(pos) => (&span[..pos], Some(&span[pos + 1..]), true),
None => (span, None, false),
};
let mut first = parse_offset(toc, Some(part1), None)?;
let mut last = if first == -1 {
parse_offset(toc, part2, Some(toc.disc_firstsector()))?
} else {
parse_offset(toc, part2, Some(first))?
};
if first == -1 {
if last == -1 {
return Err(err("Error parsing span argument"));
}
first = toc.disc_firstsector();
} else if last == -1 {
if had_hyphen {
last = toc.disc_lastsector();
} else {
let track = toc.sector_gettrack(first);
last = toc.track_lastsector(track);
}
}
Ok((first, last))
}
#[cfg(test)]
mod tests {
use super::*;
use cdrustanoia_core::device::TocEntry;
fn toc() -> DiscToc {
let mut t = DiscToc::default();
t.entries[0] = TocEntry {
flags: 0,
track: 1,
start_sector: 0,
};
t.entries[1] = TocEntry {
flags: 0,
track: 2,
start_sector: 75000,
};
t.entries[2] = TocEntry {
flags: 0,
track: 3,
start_sector: 150000,
};
t.entries[3] = TocEntry {
flags: 0x04,
track: 0xAA,
start_sector: 225000,
};
t.tracks = 3;
t
}
#[test]
fn simple_track() {
assert_eq!(parse_span(&toc(), "2").unwrap(), (75000, 149999));
}
#[test]
fn ranges() {
assert_eq!(parse_span(&toc(), "2-3").unwrap(), (75000, 224999));
assert_eq!(parse_span(&toc(), "-2").unwrap(), (0, 149999));
assert_eq!(parse_span(&toc(), "2-").unwrap(), (75000, 224999));
}
#[test]
fn times() {
assert_eq!(parse_span(&toc(), "1[1:00]").unwrap(), (4500, 74999));
assert_eq!(parse_span(&toc(), "2[::20]").unwrap(), (76500, 149999));
assert_eq!(parse_span(&toc(), "2[.30]").unwrap(), (75030, 149999));
assert_eq!(parse_span(&toc(), "2[10:]").unwrap(), (120000, 149999));
}
#[test]
fn bad() {
assert!(parse_span(&toc(), "4").is_err());
assert!(parse_span(&toc(), "1-2-3").is_err());
assert!(parse_span(&toc(), "2[1:00:00:00]").is_err());
}
}
+50
View File
@@ -0,0 +1,50 @@
use cdrustanoia_core::device::samples_to_bytes;
#[test]
fn end_to_end_image_rip() {
let dir = tempfile::tempdir().unwrap();
let image = dir.path().join("disc.raw");
let out = dir.path().join("out.raw");
let sectors = 24usize;
let mut samples = Vec::with_capacity(sectors * 1176);
for i in 0..sectors * 1176 {
samples.push(((i as i32 * 7919) % 65536 - 32768) as i16);
}
std::fs::write(&image, samples_to_bytes(&samples)).unwrap();
std::env::set_var("CDRUSTANOIA_TEST_IMAGE", &image);
let argv = vec![
"cdparanoia".to_string(),
"-q".to_string(),
"-r".to_string(),
"1".to_string(),
out.to_string_lossy().into_owned(),
];
assert_eq!(cdrustanoia_cli::run(&argv), 0);
let got = std::fs::read(&out).unwrap();
assert_eq!(got, samples_to_bytes(&samples));
std::env::remove_var("CDRUSTANOIA_TEST_IMAGE");
// Multi-track batch mode.
let multi = dir.path().join("multi.raw");
let mut multi_samples = Vec::new();
for i in 0..(48 * 1176) {
multi_samples.push((((i as i64 * 104729) % 65536) as i32 - 32768) as i16);
}
std::fs::write(&multi, samples_to_bytes(&multi_samples)).unwrap();
std::fs::write(dir.path().join("multi.raw.toc"), "0 23 0\n24 47 0\n").unwrap();
std::env::set_var("CDRUSTANOIA_TEST_IMAGE", &multi);
let argv = vec![
"cdparanoia".to_string(),
"-q".to_string(),
"-B".to_string(),
"1-2".to_string(),
format!("{}/", dir.path().display()),
];
assert_eq!(cdrustanoia_cli::run(&argv), 0);
let t1 = std::fs::read(dir.path().join("track01.cdda.wav")).unwrap();
let t2 = std::fs::read(dir.path().join("track02.cdda.wav")).unwrap();
assert_eq!(&t1[44..], &samples_to_bytes(&multi_samples[..24 * 1176]));
assert_eq!(&t2[44..], &samples_to_bytes(&multi_samples[24 * 1176..]));
std::env::remove_var("CDRUSTANOIA_TEST_IMAGE");
}
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "cdrustanoia_core"
version.workspace = true
edition.workspace = true
license = "LGPL-2.1-or-later"
[dependencies]
libc.workspace = true
[dev-dependencies]
tempfile = "3"
+16
View File
@@ -0,0 +1,16 @@
//! Paranoia callback event codes, from the LGPL cdda_paranoia.h API.
pub const PARANOIA_CB_READ: i32 = 0;
pub const PARANOIA_CB_VERIFY: i32 = 1;
pub const PARANOIA_CB_FIXUP_EDGE: i32 = 2;
pub const PARANOIA_CB_FIXUP_ATOM: i32 = 3;
pub const PARANOIA_CB_SCRATCH: i32 = 4;
pub const PARANOIA_CB_REPAIR: i32 = 5;
pub const PARANOIA_CB_SKIP: i32 = 6;
pub const PARANOIA_CB_DRIFT: i32 = 7;
pub const PARANOIA_CB_BACKOFF: i32 = 8;
pub const PARANOIA_CB_OVERLAP: i32 = 9;
pub const PARANOIA_CB_FIXUP_DROPPED: i32 = 10;
pub const PARANOIA_CB_FIXUP_DUPED: i32 = 11;
pub const PARANOIA_CB_READERR: i32 = 12;
pub const PARANOIA_CB_CACHEERR: i32 = 13;
+138
View File
@@ -0,0 +1,138 @@
//! CD-ROM device discovery, mirroring scan_devices.c's search order.
use crate::device::image::ImageDrive;
use crate::device::linux::LinuxDrive;
use crate::device::{Drive, InterfaceKind};
use std::fs;
use std::os::unix::fs::FileTypeExt;
use std::path::Path;
pub fn image_override() -> Option<ImageDrive> {
let path = std::env::var_os("CDRUSTANOIA_TEST_IMAGE")?;
ImageDrive::open_path(Path::new(&path)).ok()
}
pub fn identify_sgio(device: &str, verbose: bool) -> Result<Box<dyn Drive>, String> {
if verbose {
eprintln!("\tTesting {} for SCSI/MMC interface", device);
}
let drive = LinuxDrive::sg_io(device)?;
if verbose {
eprintln!("\t\tSG_IO device: {}", device);
eprintln!("\nCDROM model sensed sensed: {}", drive.drive_model());
}
Ok(Box::new(drive))
}
pub fn identify_cooked(device: &str, verbose: bool) -> Result<Box<dyn Drive>, String> {
if verbose {
eprintln!("\tTesting {} for cooked ioctl() interface", device);
}
let drive = LinuxDrive::cooked(device)?;
if verbose {
eprintln!("\t\tCDROM sensed: {}", drive.drive_model());
}
Ok(Box::new(drive))
}
pub fn identify_device(device: &str, verbose: bool) -> Result<Box<dyn Drive>, String> {
if let Some(img) = image_override() {
return Ok(Box::new(img));
}
if verbose {
eprintln!("Checking {} for cdrom...", device);
}
let meta = fs::metadata(device).map_err(|e| format!("Could not stat {}: {}", device, e))?;
let ft = meta.file_type();
if !ft.is_block_device() && !ft.is_char_device() {
return Err(format!("{} is not a block or character device", device));
}
let sg_err = match identify_sgio(device, verbose) {
Ok(d) => return Ok(d),
Err(e) => e,
};
match identify_cooked(device, verbose) {
Ok(d) => Ok(d),
Err(cooked_err) => Err(format!(
"Unable to identify {} (SG_IO: {}; cooked: {})",
device, sg_err, cooked_err
)),
}
}
pub fn identify(device: &str, verbose: bool, force_cooked: bool) -> Result<Box<dyn Drive>, String> {
if force_cooked {
identify_cooked(device, verbose)
} else {
identify_device(device, verbose)
}
}
pub fn find_a_cdrom(verbose: bool) -> Option<Box<dyn Drive>> {
if let Some(img) = image_override() {
return Some(Box::new(img));
}
let mut candidates: Vec<String> = Vec::new();
candidates.push("/dev/cdrom".to_string());
for prefix in [
"/dev/cdroms/cdrom",
"/dev/sr",
"/dev/scd",
"/dev/hd",
"/dev/sg",
] {
for i in 0..4 {
candidates.push(format!("{}{}", prefix, i));
candidates.push(format!("{}{}", prefix, (b'a' + i) as char));
}
}
for name in [
"/dev/cdu31a",
"/dev/cdu535",
"/dev/sbpcd",
"/dev/sonycd",
"/dev/mcd",
"/dev/sjcd",
"/dev/cm206cd",
"/dev/gscd",
"/dev/optcd",
] {
candidates.push(name.to_string());
}
let mut seen = std::collections::HashSet::new();
for dev in candidates {
if !seen.insert(dev.clone()) {
continue;
}
if !Path::new(&dev).exists() {
continue;
}
match identify_device(&dev, verbose) {
Ok(d) => {
if verbose {
eprintln!("Using {}", dev);
}
return Some(d);
}
Err(_) => {
if verbose {
eprintln!();
}
}
}
}
if verbose {
let user = std::env::var("USER").unwrap_or_else(|_| "user".to_string());
eprintln!("\n\nNo cdrom drives accessible to {} found.", user);
}
None
}
pub fn interface_name(d: &dyn Drive) -> &'static str {
match d.interface() {
InterfaceKind::SgIo => "SG_IO",
InterfaceKind::Cooked => "cooked ioctl",
InterfaceKind::Image => "image",
}
}
+323
View File
@@ -0,0 +1,323 @@
//! Synthetic image backend used by tests and by the hidden
//! `CDRUSTANOIA_TEST_IMAGE` development interface.
use super::{
bytes_to_samples, swap_raw16, DiscToc, Drive, InterfaceKind, ReadError, TimedRead, TocEntry,
CD_FRAMESIZE_RAW, CD_FRAMEWORDS, MAXTRK,
};
use std::fs;
use std::path::Path;
use std::time::Instant;
#[derive(Clone, Debug, Default)]
pub struct ImageFaults {
/// Shift the beginning of each read by up to this many PCM words.
pub jitter_words: i64,
/// Only every Nth read is jittered (0 = every read when jitter_words > 0).
pub jitter_period: u32,
/// Leave the first N reads unjittered to establish an absolute anchor.
pub jitter_skip: u32,
/// Return at most this many sectors per read (0 = no limit).
pub max_read_sectors: i64,
/// Consistent sample discontinuity at a sector boundary.
pub discontinuities: Vec<(i64, i64)>, // (sector, delta words)
/// Consistently corrupt this source range.
pub scratch: Option<(i64, i64)>, // (word offset, word length)
/// Simulated duration of a read in milliseconds.
pub read_ms: i32,
/// Simulated cache-friendly timing when reading backwards.
pub backward_ms: i32,
}
#[derive(Clone, Debug)]
pub struct ImageConfig {
pub tracks: Vec<(i64, i64, u8)>, // start sector, end sector (inclusive), flags
pub faults: ImageFaults,
pub bigendianp: i32,
pub nsectors: i64,
}
impl Default for ImageConfig {
fn default() -> Self {
Self {
tracks: Vec::new(),
faults: ImageFaults {
read_ms: 20,
backward_ms: 20,
..Default::default()
},
bigendianp: 0,
nsectors: 13,
}
}
}
pub struct ImageDrive {
samples: Vec<i16>,
toc: DiscToc,
faults: ImageFaults,
bigendianp: i32,
nsectors: i64,
opened: bool,
model: String,
read_count: u32,
last_begin: i64,
}
impl ImageDrive {
pub fn from_bytes(data: Vec<u8>) -> Self {
let sectors = (data.len() / CD_FRAMESIZE_RAW) as i64;
let tracks = if sectors > 0 {
vec![(0, sectors - 1, 0u8)]
} else {
vec![(0, 0, 0u8)]
};
Self::with_config(
data,
ImageConfig {
tracks,
..Default::default()
},
)
}
pub fn with_config(data: Vec<u8>, config: ImageConfig) -> Self {
let samples = bytes_to_samples(&data);
let mut toc = DiscToc::default();
let tracks = config.tracks;
let n = tracks.len().min(MAXTRK - 1);
for (i, (start, _end, flags)) in tracks.iter().take(n).enumerate() {
toc.entries[i] = TocEntry {
flags: *flags,
track: (i + 1) as u8,
start_sector: *start,
};
}
let leadout = if n > 0 {
tracks[n - 1].1 + 1
} else {
(data.len() / CD_FRAMESIZE_RAW) as i64
};
toc.entries[n] = TocEntry {
flags: 0x04,
track: 0xAA,
start_sector: leadout,
};
toc.tracks = n as i32;
let mut d = Self {
samples,
toc,
faults: config.faults,
bigendianp: config.bigendianp,
nsectors: config.nsectors,
opened: true,
model: "File based test interface".to_string(),
read_count: 0,
last_begin: -1,
};
d.fix_toc();
d
}
pub fn open_path(path: &Path) -> std::io::Result<Self> {
let data = fs::read(path)?;
let mut config = ImageConfig::default();
let mut sectors = (data.len() / CD_FRAMESIZE_RAW) as i64;
if sectors < 1 {
sectors = 1;
}
config.tracks = vec![(0, sectors - 1, 0)];
// Optional sidecar: one "start end flags" line per track.
let sidecar = path.with_extension(format!(
"{}.toc",
path.extension().and_then(|s| s.to_str()).unwrap_or("raw")
));
if let Ok(text) = fs::read_to_string(&sidecar) {
let mut tracks = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut fields = line.split_whitespace();
let start: i64 = match fields.next().and_then(|s| s.parse().ok()) {
Some(v) => v,
None => continue,
};
let end: i64 = fields.next().and_then(|s| s.parse().ok()).unwrap_or(start);
let flags: u8 = fields
.next()
.and_then(|s| u8::from_str_radix(s, 16).ok())
.unwrap_or(0);
tracks.push((start, end, flags));
}
if !tracks.is_empty() {
config.tracks = tracks;
}
}
let mut d = Self::with_config(data, config);
d.model = format!("Image {}", path.display());
d.opened = true;
Ok(d)
}
pub fn source_samples(&self) -> &[i16] {
&self.samples
}
fn fix_toc(&mut self) {
let tracks = self.toc.tracks.max(0) as usize;
self.toc.fixup(tracks + 1);
}
fn mapped_sample(&self, target_word: i64) -> Option<i16> {
if target_word < 0 {
return None;
}
let mut source = target_word;
for (sector, delta) in &self.faults.discontinuities {
if target_word / CD_FRAMEWORDS >= *sector {
source += *delta;
}
}
if let Some((start, len)) = self.faults.scratch {
if target_word >= start && target_word < start + len {
// A deterministic, repeated-ish garbage pattern.
let x = (target_word as u64).wrapping_mul(0x9E3779B97F4A7C15);
return Some((x >> 48) as i16);
}
}
self.samples.get(source as usize).copied()
}
fn read_words(&mut self, begin_sector: i64, sectors: i64) -> Vec<i16> {
let words = sectors.max(0) * CD_FRAMEWORDS;
let mut out = Vec::with_capacity(words as usize);
let mut jitter = 0i64;
if self.faults.jitter_words > 0 {
self.read_count = self.read_count.wrapping_add(1);
let period = if self.faults.jitter_period == 0 {
1
} else {
self.faults.jitter_period
};
if self.read_count.is_multiple_of(period) {
let max = self.faults.jitter_words;
let x = (self.read_count as u64).wrapping_mul(6364136223846793005);
jitter = (x % (2 * max as u64 + 1)) as i64 - max;
}
if self.read_count <= self.faults.jitter_skip {
jitter = 0;
}
}
let start = begin_sector * CD_FRAMEWORDS + jitter;
for w in 0..words {
out.push(self.mapped_sample(start + w).unwrap_or(0));
}
out
}
}
impl Drive for ImageDrive {
fn toc(&self) -> &DiscToc {
&self.toc
}
fn toc_mut(&mut self) -> &mut DiscToc {
&mut self.toc
}
fn opened(&self) -> bool {
self.opened
}
fn read_audio(
&mut self,
buffer: Option<&mut [u8]>,
begin: i64,
sectors: i64,
) -> Result<usize, ReadError> {
if !self.opened {
return Err(ReadError::closed());
}
if sectors <= 0 {
return Ok(0);
}
let mut sectors = sectors;
if self.faults.max_read_sectors > 0 {
sectors = sectors.min(self.faults.max_read_sectors);
}
let words = self.read_words(begin, sectors);
if let Some(buf) = buffer {
let bytes = super::samples_to_bytes(&words);
let n = bytes.len().min(buf.len());
buf[..n].copy_from_slice(&bytes[..n]);
if super::host_big_endian() {
swap_raw16(&mut buf[..n]);
}
}
self.last_begin = begin;
Ok(sectors as usize)
}
fn read_audio_raw(
&mut self,
buffer: Option<&mut [u8]>,
begin: i64,
sectors: i64,
) -> Result<usize, ReadError> {
if !self.opened {
return Err(ReadError::closed());
}
if sectors <= 0 {
return Ok(0);
}
let mut sectors = sectors;
if self.faults.max_read_sectors > 0 {
sectors = sectors.min(self.faults.max_read_sectors);
}
let words = self.read_words(begin, sectors);
if let Some(buf) = buffer {
let bytes = super::samples_to_bytes(&words);
let n = bytes.len().min(buf.len());
buf[..n].copy_from_slice(&bytes[..n]);
}
self.last_begin = begin;
Ok(sectors as usize)
}
fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead {
let start = Instant::now();
let previous_begin = self.last_begin;
let result = self.read_audio(buffer, begin, sectors);
let _elapsed = start.elapsed();
let ms = if begin < previous_begin {
self.faults.backward_ms
} else {
self.faults.read_ms
};
TimedRead {
result,
milliseconds: ms,
}
}
fn speed_set(&mut self, _speed: i32) -> i32 {
0
}
fn close(&mut self) {
self.opened = false;
}
fn drive_model(&self) -> &str {
&self.model
}
fn bigendianp(&self) -> i32 {
self.bigendianp
}
fn set_bigendianp(&mut self, value: i32) {
self.bigendianp = value;
}
fn nsectors(&self) -> i64 {
self.nsectors
}
fn set_nsectors(&mut self, value: i64) {
self.nsectors = value;
}
fn interface(&self) -> InterfaceKind {
InterfaceKind::Image
}
}
File diff suppressed because it is too large Load Diff
+422
View File
@@ -0,0 +1,422 @@
//! Common CD-ROM device abstractions and TOC helpers.
//!
//! The layout and behaviour intentionally mirror cdparanoia 10.2's
//! `cdda_interface` API. Device specific backends live in sibling modules.
pub mod discover;
pub mod image;
pub mod linux;
use std::fmt;
pub const CD_FRAMESIZE_RAW: usize = 2352;
pub const CD_FRAMESIZE: usize = 2048;
pub const CD_FRAMEWORDS: i64 = 1176;
pub const CD_FRAMESAMPLES: usize = CD_FRAMESIZE_RAW / 4;
pub const MAXTRK: usize = 100;
/// Error codes used by the original cdda_interface library.
pub mod error {
pub const DEVICE_NOT_OPEN: i64 = -400;
pub const INVALID_TRACK: i64 = -401;
pub const TRACK_NOT_AUDIO: i64 = -402;
pub const NO_AUDIO_TRACKS: i64 = -403;
pub const NO_MEDIUM: i64 = -404;
pub const OPTION_NOT_SUPPORTED: i64 = -405;
pub const INTERFACE_NOT_SUPPORTED: i64 = -100;
pub const PERMISSION_IOCTL: i64 = -102;
pub const PERMISSION_DATA: i64 = -103;
pub const KERNEL_MEMORY: i64 = -300;
pub const CANT_SET_CDDA: i64 = -1;
pub const CANT_READ_LEADOUT: i64 = -2;
pub const ILLEGAL_TRACKS: i64 = -3;
pub const CANT_READ_TOC_HEADER: i64 = -4;
pub const CANT_READ_TOC_ENTRY: i64 = -5;
pub const CANT_READ_ANY_DATA: i64 = -6;
pub const UNKNOWN_READ: i64 = -7;
pub const CANT_IDENTIFY: i64 = -8;
pub const ILLEGAL_TOC: i64 = -9;
pub const UNREADABLE_SECTOR: i64 = -10;
}
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
pub struct TocEntry {
pub flags: u8,
pub track: u8,
pub start_sector: i64,
}
impl TocEntry {
pub fn audio(&self) -> bool {
self.flags & 0x04 == 0
}
pub fn copyp(&self) -> bool {
self.flags & 0x02 != 0
}
pub fn preemp(&self) -> bool {
self.flags & 0x01 != 0
}
pub fn channels(&self) -> i32 {
if self.flags & 0x08 != 0 {
4
} else {
2
}
}
}
#[derive(Clone, Debug)]
pub struct DiscToc {
pub entries: [TocEntry; MAXTRK],
pub tracks: i32,
pub cd_extra: i32,
}
impl Default for DiscToc {
fn default() -> Self {
Self {
entries: [TocEntry::default(); MAXTRK],
tracks: 0,
cd_extra: 0,
}
}
}
impl DiscToc {
pub fn track_firstsector(&self, track: i32) -> i64 {
if self.tracks <= 0 {
return error::DEVICE_NOT_OPEN;
}
if track == 0 {
if self.entries[0].start_sector == 0 {
return error::INVALID_TRACK;
}
return 0;
}
if track < 0 || track as usize > self.tracks as usize {
return error::INVALID_TRACK;
}
self.entries[track as usize - 1].start_sector
}
pub fn track_lastsector(&self, track: i32) -> i64 {
if self.tracks <= 0 {
return error::DEVICE_NOT_OPEN;
}
if track == 0 {
if self.entries[0].start_sector == 0 {
return error::INVALID_TRACK;
}
return self.entries[0].start_sector - 1;
}
if track < 1 || track as usize > self.tracks as usize {
return error::INVALID_TRACK;
}
self.entries[track as usize].start_sector - 1
}
pub fn disc_firstsector(&self) -> i64 {
if self.tracks <= 0 {
return error::DEVICE_NOT_OPEN;
}
for i in 0..self.tracks as usize {
if self.entries[i].audio() {
if i == 0 {
return 0;
}
return self.entries[i].start_sector;
}
}
error::NO_AUDIO_TRACKS
}
pub fn disc_lastsector(&self) -> i64 {
if self.tracks <= 0 {
return error::DEVICE_NOT_OPEN;
}
for i in (0..self.tracks as usize).rev() {
if self.entries[i].audio() {
return self.entries[i + 1].start_sector - 1;
}
}
error::NO_AUDIO_TRACKS
}
pub fn sector_gettrack(&self, sector: i64) -> i32 {
if self.tracks <= 0 {
return error::DEVICE_NOT_OPEN as i32;
}
if sector < self.entries[0].start_sector {
return 0;
}
for i in 0..self.tracks as usize {
if self.entries[i].start_sector <= sector && self.entries[i + 1].start_sector > sector {
return (i + 1) as i32;
}
}
error::INVALID_TRACK as i32
}
pub fn track_bitmap(&self, track: i32, bit: u8, set: i32, clear: i32) -> i32 {
let track = if track == 0 { 1 } else { track };
if track < 1 || track as usize > self.tracks as usize {
return error::INVALID_TRACK as i32;
}
if self.entries[track as usize - 1].flags & bit != 0 {
set
} else {
clear
}
}
pub fn track_channels(&self, track: i32) -> i32 {
self.track_bitmap(track, 8, 4, 2)
}
pub fn track_audiop(&self, track: i32) -> i32 {
self.track_bitmap(track, 4, 0, 1)
}
pub fn track_copyp(&self, track: i32) -> i32 {
self.track_bitmap(track, 2, 1, 0)
}
pub fn track_preemp(&self, track: i32) -> i32 {
self.track_bitmap(track, 1, 1, 0)
}
pub fn is_audio_index(&self, idx: usize, tracks: usize) -> bool {
if idx >= tracks {
return false;
}
self.entries[idx].audio()
}
/// Reproduces `FixupTOC`'s non-ioctl repairs.
pub fn fixup(&mut self, tracks: usize) {
for j in 0..tracks {
if self.entries[j].start_sector < 0 {
self.entries[j].start_sector = 0;
}
if j + 1 < tracks && self.entries[j].start_sector > self.entries[j + 1].start_sector {
self.entries[j].start_sector = 0;
}
}
if tracks == 0 {
return;
}
let mut last = self.entries[0].start_sector;
for j in 1..tracks {
if self.entries[j].start_sector < last {
self.entries[j].start_sector = last;
}
last = self.entries[j].start_sector;
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InterfaceKind {
SgIo,
Cooked,
Image,
}
#[derive(Debug, Clone)]
pub struct ReadError {
pub errno: i32,
pub message: String,
}
impl ReadError {
pub fn new(errno: i32, message: impl Into<String>) -> Self {
Self {
errno,
message: message.into(),
}
}
pub fn no_medium() -> Self {
Self::new(libc::ENOMEDIUM, "No medium present")
}
pub fn closed() -> Self {
Self::new(libc::EBADF, "Device not open")
}
pub fn io(message: impl Into<String>) -> Self {
Self::new(libc::EIO, message)
}
}
impl fmt::Display for ReadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (errno {})", self.message, self.errno)
}
}
#[derive(Debug, Clone)]
pub struct TimedRead {
pub result: Result<usize, ReadError>,
pub milliseconds: i32,
}
/// Backend interface used by both the command line program and the C ABI.
///
/// Positions are absolute CD sectors (not sample positions). Audio data is
/// returned in host byte order, exactly like `cdda_read`.
pub trait Drive {
fn toc(&self) -> &DiscToc;
fn toc_mut(&mut self) -> &mut DiscToc;
fn opened(&self) -> bool;
/// Perform the media-dependent part of opening a drive (TOC read,
/// CDDA capability probe, endianness detection). Identify-only
/// operations leave this undone, just like cdda_identify/cdda_open.
fn open_disc(&mut self) -> Result<(), String> {
Ok(())
}
fn read_audio(
&mut self,
buffer: Option<&mut [u8]>,
begin: i64,
sectors: i64,
) -> Result<usize, ReadError>;
/// Low-level read without host-endian conversion, matching the original
/// `cdrom_drive::read_audio` function pointer.
fn read_audio_raw(
&mut self,
buffer: Option<&mut [u8]>,
begin: i64,
sectors: i64,
) -> Result<usize, ReadError> {
self.read_audio(buffer, begin, sectors)
}
fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead;
fn speed_set(&mut self, speed: i32) -> i32;
fn close(&mut self);
fn drive_model(&self) -> &str;
fn bigendianp(&self) -> i32;
fn set_bigendianp(&mut self, value: i32);
fn nsectors(&self) -> i64;
fn set_nsectors(&mut self, value: i64);
fn interface(&self) -> InterfaceKind;
fn cdda_version(&self) -> &'static str {
crate::VERSIONNUM
}
// Convenience TOC wrappers.
fn tracks(&self) -> i32 {
self.toc().tracks
}
fn track_firstsector(&self, track: i32) -> i64 {
self.toc().track_firstsector(track)
}
fn track_lastsector(&self, track: i32) -> i64 {
self.toc().track_lastsector(track)
}
fn disc_firstsector(&self) -> i64 {
self.toc().disc_firstsector()
}
fn disc_lastsector(&self) -> i64 {
self.toc().disc_lastsector()
}
fn sector_gettrack(&self, sector: i64) -> i32 {
self.toc().sector_gettrack(sector)
}
fn track_audiop(&self, track: i32) -> bool {
self.toc().track_audiop(track) == 1
}
fn track_copyp(&self, track: i32) -> bool {
self.toc().track_copyp(track) == 1
}
fn track_preemp(&self, track: i32) -> bool {
self.toc().track_preemp(track) == 1
}
fn track_channels(&self, track: i32) -> i32 {
self.toc().track_channels(track)
}
}
pub fn swap16(x: i16) -> i16 {
(x as u16).swap_bytes() as i16
}
pub fn host_big_endian() -> bool {
cfg!(target_endian = "big")
}
/// Swap a buffer of host endian samples to the requested byte order.
pub fn swap_samples(buffer: &mut [i16], target_big_endian: bool) {
if host_big_endian() != target_big_endian {
for s in buffer {
*s = swap16(*s);
}
}
}
/// Convert raw CDDA bytes into host endian i16 samples.
pub fn bytes_to_samples(bytes: &[u8]) -> Vec<i16> {
let mut out = Vec::with_capacity(bytes.len() / 2);
for chunk in bytes.chunks_exact(2) {
out.push(i16::from_ne_bytes([chunk[0], chunk[1]]));
}
out
}
pub fn samples_to_bytes(samples: &[i16]) -> Vec<u8> {
let mut out = Vec::with_capacity(samples.len() * 2);
for s in samples {
out.extend_from_slice(&s.to_ne_bytes());
}
out
}
/// Swap bytes in a raw buffer between little and big endian 16-bit words.
pub fn swap_raw16(buffer: &mut [u8]) {
for chunk in buffer.chunks_exact_mut(2) {
chunk.swap(0, 1);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn toc() -> DiscToc {
let mut t = DiscToc::default();
t.tracks = 2;
t.entries[0] = TocEntry {
flags: 0,
track: 1,
start_sector: 0,
};
t.entries[1] = TocEntry {
flags: 0,
track: 2,
start_sector: 100,
};
t.entries[2] = TocEntry {
flags: 4,
track: 0xAA,
start_sector: 200,
};
t
}
#[test]
fn toc_queries() {
let t = toc();
assert_eq!(t.track_firstsector(1), 0);
assert_eq!(t.track_lastsector(1), 99);
assert_eq!(t.track_firstsector(2), 100);
assert_eq!(t.track_lastsector(2), 199);
assert_eq!(t.disc_firstsector(), 0);
assert_eq!(t.disc_lastsector(), 199);
assert_eq!(t.sector_gettrack(0), 1);
assert_eq!(t.sector_gettrack(99), 1);
assert_eq!(t.sector_gettrack(100), 2);
assert_eq!(t.track_audiop(1), 1);
assert_eq!(t.track_audiop(2), 1);
}
#[test]
fn byte_conversion() {
let s = [1i16, -2, 300];
let b = samples_to_bytes(&s);
assert_eq!(bytes_to_samples(&b), s);
}
}
+27
View File
@@ -0,0 +1,27 @@
#![allow(
clippy::too_many_arguments,
clippy::missing_safety_doc,
clippy::while_let_loop,
clippy::unnecessary_cast,
clippy::field_reassign_with_default
)]
//! cdrustanoia core library.
//!
//! This is a Rust port of the cdparanoia III 10.2 interface and paranoia
//! libraries, plus the command line helper code.
pub mod callback;
pub mod device;
pub mod paranoia;
pub const VERSIONNUM: &str = "10.2";
pub const VERSION: &str = "cdparanoia III release 10.2 (September 11, 2008)";
pub fn paranoia_version() -> &'static str {
VERSIONNUM
}
pub fn cdda_version() -> &'static str {
VERSIONNUM
}
+195
View File
@@ -0,0 +1,195 @@
//! c_block/v_fragment/root data structures and vector operations.
pub const FLAGS_EDGE: u8 = 0x1;
pub const FLAGS_UNREAD: u8 = 0x2;
pub const FLAGS_VERIFIED: u8 = 0x4;
#[derive(Clone, Debug)]
pub struct CBlock {
pub vector: Vec<i16>,
pub flags: Option<Vec<u8>>,
pub begin: i64,
pub size: i64,
pub lastsector: bool,
}
impl Default for CBlock {
fn default() -> Self {
Self::new()
}
}
impl CBlock {
pub fn new() -> Self {
Self {
vector: Vec::new(),
flags: None,
begin: 0,
size: 0,
lastsector: false,
}
}
pub fn from_parts(vector: Vec<i16>, begin: i64, lastsector: bool) -> Self {
let size = vector.len() as i64;
Self {
vector,
flags: None,
begin,
size,
lastsector,
}
}
#[inline]
pub fn end(&self) -> i64 {
self.begin + self.size
}
#[inline]
pub fn begin(&self) -> i64 {
self.begin
}
#[inline]
pub fn size(&self) -> i64 {
self.size
}
pub fn insert(&mut self, pos: i64, data: &[i16]) {
let vs = self.size;
if pos < 0 || pos > vs {
return;
}
let pos = pos as usize;
self.vector.splice(pos..pos, data.iter().copied());
self.size += data.len() as i64;
if let Some(flags) = self.flags.as_mut() {
let n = data.len();
let pos = pos.min(flags.len());
flags.splice(pos..pos, std::iter::repeat_n(0, n));
}
}
pub fn remove(&mut self, cutpos: i64, mut cutsize: i64) {
let vs = self.size;
if cutpos < 0 || cutpos > vs {
return;
}
if cutpos + cutsize > vs {
cutsize = vs - cutpos;
}
if cutsize < 0 {
cutsize = vs - cutpos;
}
if cutsize < 1 {
return;
}
let pos = cutpos as usize;
let n = cutsize as usize;
self.vector.drain(pos..pos + n);
self.size -= cutsize;
if let Some(flags) = self.flags.as_mut() {
let pos = pos.min(flags.len());
let end = (pos + n).min(flags.len());
flags.drain(pos..end);
}
}
pub fn overwrite(&mut self, pos: i64, data: &[i16]) {
if pos < 0 {
return;
}
let mut size = data.len() as i64;
if pos + size > self.size {
size = self.size - pos;
}
if size <= 0 {
return;
}
let p = pos as usize;
let n = size as usize;
self.vector[p..p + n].copy_from_slice(&data[..n]);
}
pub fn append(&mut self, data: &[i16]) {
self.vector.extend_from_slice(data);
self.size += data.len() as i64;
if let Some(flags) = self.flags.as_mut() {
flags.extend(std::iter::repeat_n(0, data.len()));
}
}
pub fn remove_front(&mut self, cut: i64) {
if cut <= 0 {
return;
}
let n = (cut as usize).min(self.vector.len());
self.vector.drain(..n);
self.size -= n as i64;
self.begin += n as i64;
if let Some(flags) = self.flags.as_mut() {
let n = n.min(flags.len());
flags.drain(..n);
}
}
pub fn flag_at(&self, abs_word: i64) -> u8 {
let idx = abs_word - self.begin;
if idx < 0 {
return 0;
}
self.flags
.as_ref()
.and_then(|f| f.get(idx as usize).copied())
.unwrap_or(0)
}
}
#[derive(Clone, Debug)]
pub struct VFragment {
pub vector: Vec<i16>,
pub begin: i64,
pub size: i64,
pub lastsector: bool,
}
impl VFragment {
pub fn end(&self) -> i64 {
self.begin + self.size
}
}
#[derive(Clone, Debug, Default)]
pub struct RootBlock {
pub returned_limit: i64,
pub lastsector: bool,
pub vector: Option<CBlock>,
pub silence_flag: bool,
pub silence_begin: i64,
}
impl RootBlock {
#[inline]
pub fn begin(&self) -> i64 {
self.vector.as_ref().map(|v| v.begin).unwrap_or(-1)
}
#[inline]
pub fn end(&self) -> i64 {
self.vector.as_ref().map(|v| v.end()).unwrap_or(-1)
}
#[inline]
pub fn is_empty(&self) -> bool {
self.vector.is_none()
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Offsets {
pub offpoints: i64,
pub newpoints: i64,
pub offaccum: i64,
pub offdiff: i64,
pub offmin: i64,
pub offmax: i64,
}
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
//! Sample-value index, a safe Rust equivalent of isort.c.
use std::collections::HashMap;
pub struct SortIndex {
buckets: HashMap<i16, Vec<u32>>,
pub begin: i64,
pub size: usize,
cursor: Option<(i16, usize, usize)>, // value, next bucket index, hi
}
impl SortIndex {
pub fn new(_maxsize: usize) -> Self {
Self {
buckets: HashMap::new(),
begin: 0,
size: 0,
cursor: None,
}
}
pub fn setup(&mut self, samples: &[i16], begin_abs: i64, sortlo: i64, sorthi: i64) {
self.buckets.clear();
self.begin = begin_abs;
self.size = samples.len();
self.cursor = None;
let lo = ((sortlo - begin_abs).max(0) as usize).min(samples.len());
let hi = ((sorthi - begin_abs).max(0) as usize).min(samples.len());
if lo >= hi {
return;
}
for (i, sample) in samples.iter().enumerate().take(hi).skip(lo) {
self.buckets.entry(*sample).or_default().push(i as u32);
}
}
/// Equivalent of sort_getmatch(). Returns the first matching relative
/// position, and leaves a cursor for sort_nextmatch().
pub fn getmatch(&mut self, post: i64, overlap: i64, value: i16) -> Option<usize> {
let post = post.clamp(0, self.size as i64);
let lo = (post - overlap).max(0) as usize;
let hi = ((post + overlap).min(self.size as i64)).max(0) as usize;
self.cursor = None;
let positions = self.buckets.get(&value)?;
let idx = positions.partition_point(|p| (*p as usize) < lo);
if idx >= positions.len() {
return None;
}
let pos = positions[idx] as usize;
if pos >= hi {
return None;
}
self.cursor = Some((value, idx + 1, hi));
Some(pos)
}
pub fn nextmatch(&mut self) -> Option<usize> {
let (value, idx, hi) = self.cursor?;
let positions = self.buckets.get(&value)?;
if idx >= positions.len() {
self.cursor = None;
return None;
}
let pos = positions[idx] as usize;
if pos >= hi {
self.cursor = None;
return None;
}
self.cursor = Some((value, idx + 1, hi));
Some(pos)
}
/// Position most recently returned by getmatch()/advance().
pub fn cursor_position(&self) -> Option<i64> {
let (value, idx, _hi) = self.cursor?;
let positions = self.buckets.get(&value)?;
if idx == 0 || idx > positions.len() {
return None;
}
Some(positions[idx - 1] as i64)
}
/// Advance to the next match. Returns false when exhausted.
pub fn advance(&mut self) -> bool {
self.nextmatch().is_some()
}
}
+130
View File
@@ -0,0 +1,130 @@
use cdrustanoia_core::device::image::ImageDrive;
use cdrustanoia_core::device::{bytes_to_samples, samples_to_bytes, Drive, CD_FRAMESIZE_RAW};
use cdrustanoia_core::paranoia::{Paranoia, PARANOIA_MODE_FULL};
fn make_source(sectors: usize) -> Vec<i16> {
let mut out = Vec::with_capacity(sectors * 1176);
let mut x: u64 = 0x1234_5678_9abc_def0;
for i in 0..sectors * 1176 {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
// Mix in some silence and a distinct waveform.
let v = if i % 5000 < 1000 {
0
} else {
(x as i16).wrapping_add((i as i16).wrapping_mul(7))
};
out.push(v);
}
out
}
#[test]
fn clean_image_roundtrip() {
let sectors = 80usize;
let source = make_source(sectors);
let bytes = samples_to_bytes(&source);
let drive = ImageDrive::from_bytes(bytes);
let mut p = Paranoia::new(Box::new(drive));
p.modeset(PARANOIA_MODE_FULL);
let mut got = Vec::with_capacity(sectors * 1176);
let mut cb = |_: i64, _: i32| {};
for _ in 0..sectors {
let sec = p.read(&mut cb).expect("read sector");
got.extend_from_slice(sec);
}
assert_eq!(got.len(), source.len());
assert_eq!(got, source);
}
#[test]
fn drive_toc_helpers() {
let bytes = vec![0u8; CD_FRAMESIZE_RAW * 10];
let drive = ImageDrive::from_bytes(bytes);
assert_eq!(drive.tracks(), 1);
assert_eq!(drive.disc_firstsector(), 0);
assert_eq!(drive.disc_lastsector(), 9);
assert_eq!(drive.track_firstsector(1), 0);
assert_eq!(drive.track_lastsector(1), 9);
let _ = bytes_to_samples(&[0, 0]);
}
fn run_config(sectors: usize, config: cdrustanoia_core::device::image::ImageConfig) -> Vec<i16> {
let source = make_source(sectors);
let bytes = samples_to_bytes(&source);
let drive = ImageDrive::with_config(bytes, config);
let mut p = Paranoia::new(Box::new(drive));
p.modeset(PARANOIA_MODE_FULL);
let start = 5usize.min(sectors);
p.seek(start as i64, 0);
let mut got = Vec::with_capacity((sectors - start) * 1176);
let mut cb = |_: i64, _: i32| {};
for _ in start..sectors {
let sec = p.read(&mut cb).expect("read sector");
got.extend_from_slice(sec);
}
let source = &source[start * 1176..];
assert_eq!(got.len(), source.len());
if got != source {
let mut best: Option<(i64, usize)> = None;
for shift in -2000i64..=2000 {
let mut bad = 0usize;
for (i, value) in got.iter().enumerate().take(source.len()) {
let j = i as i64 + shift;
if j < 0 || j as usize >= source.len() || *value != source[j as usize] {
bad += 1;
if bad > 8 {
break;
}
}
}
if bad <= 8 {
best = Some((shift, bad));
break;
}
}
if let Some((shift, bad)) = best {
panic!(
"data shifted by {} words ({} mismatches); engine did not align absolute position",
shift, bad
);
}
let idx = got
.iter()
.zip(source.iter())
.position(|(a, b)| a != b)
.unwrap_or(0);
let where0 = source.iter().position(|x| *x == got[0]);
panic!(
"mismatch at word {} (sector {} offset {}): got {} expected {}; got0 found at {:?}; got[..8]={:?} src[..8]={:?}",
idx,
idx / 1176,
idx % 1176,
got[idx],
source[idx],
where0,
&got[..got.len().min(8)],
&source[..source.len().min(8)]
);
}
got
}
#[test]
fn jittered_image_roundtrip() {
use cdrustanoia_core::device::image::{ImageConfig, ImageFaults};
let sectors = 20usize;
let config = ImageConfig {
tracks: vec![(0, sectors as i64 - 1, 0)],
faults: ImageFaults {
jitter_words: 180,
jitter_period: 1,
jitter_skip: 10,
max_read_sectors: 0,
..Default::default()
},
..Default::default()
};
run_config(sectors, config);
}