#![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; fn ffi_guard(default: T, f: impl FnOnce() -> T) -> T { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { Ok(value) => value, Err(_) => default, } } 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 { 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 { ffi_guard(ptr::null_mut(), || paranoia_init_impl(d)) } unsafe fn paranoia_init_impl(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) { ffi_guard((), || paranoia_free_impl(p)) } unsafe fn paranoia_free_impl(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 { ffi_guard(ptr::null_mut(), || { paranoia_read_limited_impl(p, callback, max_retries) }) } unsafe fn paranoia_read_limited_impl( 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); } } }