first draft
This commit is contained in:
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libcdda_interface.so.0");
|
||||
}
|
||||
@@ -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) {}
|
||||
@@ -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" }
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libcdda_paranoia.so.0");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
fn main() {
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
std::process::exit(cdrustanoia_cli::run(&argv));
|
||||
}
|
||||
@@ -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(§ors) {
|
||||
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
|
||||
}
|
||||
@@ -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)*))
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
fn main() {
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
std::process::exit(cdrustanoia_cli::run(&argv));
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user