#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces,
unused_qualifications,
clippy::manual_assert
)]
#![warn(
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::must_use_candidate,
clippy::ptr_cast_constness,
clippy::range_plus_one,
clippy::undocumented_unsafe_blocks
)]
use discid_sys::*;
use std::error::Error;
use std::ffi::{CStr, CString};
use std::fmt;
use std::num::ParseIntError;
use std::os::raw::c_char;
use std::os::raw::c_int;
use std::ptr;
use std::rc::Rc;
#[macro_use]
extern crate bitflags;
bitflags! {
#[derive(PartialEq, Debug)]
pub struct Features: u32 {
const READ = discid_feature::DISCID_FEATURE_READ.0;
const MCN = discid_feature::DISCID_FEATURE_MCN.0;
const ISRC = discid_feature::DISCID_FEATURE_ISRC.0;
}
}
impl From<Features> for discid_feature {
fn from(item: Features) -> Self {
Self(item.bits())
}
}
#[derive(Debug)]
#[must_use]
pub struct DiscError {
reason: String,
}
impl DiscError {
fn new(message: &str) -> Self {
Self {
reason: message.to_string(),
}
}
}
impl Error for DiscError {}
impl fmt::Display for DiscError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DiscError: {}", self.reason)
}
}
impl From<ParseIntError> for DiscError {
fn from(err: ParseIntError) -> Self {
Self {
reason: err.to_string(),
}
}
}
#[derive(Debug)]
#[must_use]
struct DiscIdHandle {
real_handle: ptr::NonNull<discid_sys::DiscId>,
}
impl DiscIdHandle {
fn new(handle: *mut discid_sys::DiscId) -> Result<Self, DiscError> {
if handle.is_null() {
Err(DiscError::new("handle must not be null"))
} else {
unsafe {
Ok(Self {
real_handle: ptr::NonNull::new_unchecked(handle),
})
}
}
}
fn as_ptr(&self) -> *mut discid_sys::DiscId {
self.real_handle.as_ptr()
}
}
impl Drop for DiscIdHandle {
fn drop(&mut self) {
unsafe { discid_free(self.as_ptr()) }
}
}
#[must_use]
pub struct DiscId {
handle: Rc<DiscIdHandle>,
}
impl DiscId {
fn new() -> Result<Self, DiscError> {
let handle = unsafe { discid_new() };
if handle.is_null() {
Err(DiscError::new(
"discid_new() failed, could not allocate memory",
))
} else {
let result = DiscIdHandle::new(handle);
match result {
Ok(handle_t) => Ok(Self {
handle: Rc::new(handle_t),
}),
Err(err) => Err(err),
}
}
}
pub fn read(device: Option<&str>) -> Result<Self, DiscError> {
Self::read_features(device, Features::READ)
}
pub fn read_features(device: Option<&str>, features: Features) -> Result<Self, DiscError> {
let disc = Self::new()?;
let c_device = device.map(|d| CString::new(d).unwrap_or_default());
let c_device_ptr = c_device.as_ref().map_or(ptr::null(), |c| c.as_ptr());
let status =
unsafe { discid_read_sparse(disc.handle.as_ptr(), c_device_ptr, features.bits()) };
if status == 0 {
Err(disc.error())
} else {
Ok(disc)
}
}
pub fn put(first: i32, offsets: &[i32]) -> Result<Self, DiscError> {
let disc = Self::new()?;
let last = first + offsets.len() as i32 - 2;
let offset_ptr: *mut c_int;
let mut full_offsets: [c_int; 100];
if first > 1 && last <= 99 {
full_offsets = [0; 100];
full_offsets[0] = offsets[0]; full_offsets[first as usize..=last as usize].copy_from_slice(&offsets[1..]);
offset_ptr = full_offsets.as_mut_ptr();
} else {
offset_ptr = offsets.as_ptr().cast_mut();
}
let status = unsafe { discid_put(disc.handle.as_ptr(), first, last, offset_ptr) };
if status == 0 {
Err(disc.error())
} else {
Ok(disc)
}
}
pub fn parse(toc: &str) -> Result<Self, DiscError> {
let mut i: usize = 0;
let mut first_track: c_int = 1;
let mut last_track: c_int = 1;
let mut offsets: [c_int; 100] = [0; 100];
for part in toc.split(' ') {
let parsed_int = part.parse::<c_int>()?;
if i == 0 {
first_track = parsed_int;
} else if i == 1 {
last_track = parsed_int;
} else if i > 1 {
if i > (last_track as usize + 2) || i > 99 + 2 {
return Err(DiscError::new(
"TOC string contains too many offsets (max. 100)",
));
}
offsets[i - 2] = parsed_int;
}
i += 1;
}
if i < 3 {
return Err(DiscError::new(&format!("Invalid TOC string {toc:?}")));
}
let offset_count = (i - 3) as c_int;
let track_count = last_track - first_track + 1;
if track_count != offset_count {
return Err(DiscError::new(&format!(
"Number of offsets {offset_count} does not match track count {track_count}",
)));
}
Self::put(first_track, &offsets[0..(i - 2)])
}
#[must_use]
pub fn has_feature(feature: Features) -> bool {
for feature in feature.iter() {
let result = unsafe { discid_has_feature(feature.into()) };
if result != 1 {
return false;
}
}
true
}
#[must_use]
pub fn version_string() -> String {
let str_ptr = unsafe { discid_get_version_string() };
to_str(str_ptr)
}
#[must_use]
pub fn default_device() -> String {
let version_ptr = unsafe { discid_get_default_device() };
to_str(version_ptr)
}
fn error(&self) -> DiscError {
let str_ptr = unsafe { discid_get_error_msg(self.handle.as_ptr()) };
DiscError::new(&to_str(str_ptr))
}
#[must_use]
pub fn id(&self) -> String {
let str_ptr = unsafe { discid_get_id(self.handle.as_ptr()) };
to_str(str_ptr)
}
#[must_use]
pub fn freedb_id(&self) -> String {
let str_ptr = unsafe { discid_get_freedb_id(self.handle.as_ptr()) };
to_str(str_ptr)
}
#[must_use]
pub fn toc_string(&self) -> String {
let str_ptr = unsafe { discid_get_toc_string(self.handle.as_ptr()) };
to_str(str_ptr)
}
#[must_use]
pub fn submission_url(&self) -> String {
let str_ptr = unsafe { discid_get_submission_url(self.handle.as_ptr()) };
to_str(str_ptr)
}
#[must_use]
pub fn first_track_num(&self) -> i32 {
unsafe { discid_get_first_track_num(self.handle.as_ptr()) }
}
#[must_use]
pub fn last_track_num(&self) -> i32 {
unsafe { discid_get_last_track_num(self.handle.as_ptr()) }
}
#[must_use]
pub fn sectors(&self) -> i32 {
unsafe { discid_get_sectors(self.handle.as_ptr()) }
}
#[must_use]
pub fn mcn(&self) -> String {
let str_ptr = unsafe { discid_get_mcn(self.handle.as_ptr()) };
to_str(str_ptr)
}
pub fn tracks(&self) -> TrackIter {
TrackIter::new(Rc::clone(&self.handle))
}
#[must_use]
pub fn get_track(&self, number: i32) -> Option<Track> {
if self.is_valid_track_number(number) {
Some(get_track(Rc::clone(&self.handle), number))
} else {
None
}
}
pub fn nth_track(&self, number: i32) -> Track {
assert!(
self.is_valid_track_number(number),
"track number out of bounds: {number}"
);
get_track(Rc::clone(&self.handle), number)
}
fn is_valid_track_number(&self, number: i32) -> bool {
let first = self.first_track_num();
let last = self.last_track_num();
number >= first && number <= last
}
}
impl fmt::Debug for DiscId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DiscId {}", self.toc_string())
}
}
#[derive(Debug)]
#[must_use]
pub struct Track {
pub number: i32,
pub offset: i32,
pub sectors: i32,
pub isrc: String,
}
#[derive(Debug)]
#[must_use]
pub struct TrackIter {
handle: Rc<DiscIdHandle>,
curr: i32,
last_track: i32,
}
impl TrackIter {
fn new(handle: Rc<DiscIdHandle>) -> Self {
let handle_ptr = handle.as_ptr();
let first_track = unsafe { discid_get_first_track_num(handle_ptr) };
let last_track = unsafe { discid_get_last_track_num(handle_ptr) };
Self {
handle,
curr: first_track,
last_track,
}
}
}
impl Iterator for TrackIter {
type Item = Track;
fn next(&mut self) -> Option<Track> {
let track_num = self.curr;
self.curr += 1;
if track_num <= self.last_track {
Some(get_track(Rc::clone(&self.handle), track_num))
} else {
None
}
}
}
fn get_track(handle: Rc<DiscIdHandle>, number: i32) -> Track {
let handle_ptr = handle.as_ptr();
let isrc_ptr = unsafe { discid_get_track_isrc(handle_ptr, number) };
Track {
number,
offset: unsafe { discid_get_track_offset(handle_ptr, number) },
sectors: unsafe { discid_get_track_length(handle_ptr, number) },
isrc: to_str(isrc_ptr),
}
}
fn to_str(c_buf: *const c_char) -> String {
if c_buf.is_null() {
return String::new();
}
let c_str: &CStr = unsafe { CStr::from_ptr(c_buf) };
let str_slice = c_str.to_string_lossy();
str_slice.into_owned()
}
#[cfg(test)]
mod tests {
use super::{DiscError, DiscId, Features, Track};
#[test]
#[ignore = "requires a CD inserted into the default CD drive"]
fn discid_read_default_device() {
let disc = DiscId::read(None).expect("DiscId::read failed");
assert_eq!(28, disc.id().len());
}
#[test]
fn discid_read_invalid_device() {
let err = DiscId::read(Some("notadevice")).expect_err("DiscId::read failed");
assert!(err.to_string().contains("cannot open"));
}
#[test]
#[ignore = "requires a CD containing MCN and ISRC info inserted into the default CD drive"]
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
fn discid_read_features_all() {
let disc =
DiscId::read_features(None, Features::all()).expect("DiscId::read_features failed");
assert_eq!(28, disc.id().len());
assert!(!disc.mcn().is_empty());
assert_eq!(12, disc.nth_track(1).isrc.len());
}
#[test]
fn discid_read_features_invalid_device() {
let err = DiscId::read_features(Some("notadevice"), Features::READ)
.expect_err("DiscId::read_features failed");
assert!(err.to_string().contains("cannot open"));
}
#[test]
fn discid_put() {
let first = 1;
let offsets = [
206535, 150, 18901, 39738, 59557, 79152, 100126, 124833, 147278, 166336, 182560,
];
let disc = DiscId::put(first, &offsets).expect("DiscId::put failed");
let last_track = disc.last_track_num();
assert_eq!("Wn8eRBtfLDfM0qjYPdxrz.Zjs_U-", disc.id());
assert_eq!("830abf0a", disc.freedb_id());
assert_eq!(1, disc.first_track_num());
assert_eq!(10, last_track);
assert_eq!(206535, disc.sectors());
assert_eq!(
"1 10 206535 150 18901 39738 59557 79152 100126 124833 147278 166336 182560",
disc.toc_string()
);
assert_eq!(
"http://musicbrainz.org/cdtoc/attach?id=Wn8eRBtfLDfM0qjYPdxrz.Zjs_U-&tracks=10&toc=1+10+206535+150+18901+39738+59557+79152+100126+124833+147278+166336+182560",
disc.submission_url()
);
for track in disc.tracks() {
let offset = offsets[track.number as usize];
let next = if track.number < last_track {
track.number + 1
} else {
0
};
let length = offsets[next as usize] - offset;
assert_eq!(
offset, track.offset,
"track {} expected offset {}",
track.number, offset
);
assert_eq!(
length, track.sectors,
"track {} expected sectors {}",
track.number, length
);
}
}
#[test]
fn discid_put_first_track_not_one() {
let first = 3;
let offsets = [
206535, 150, 18901, 39738, 59557, 79152, 100126, 124833, 147278, 166336, 182560,
];
let disc = DiscId::put(first, &offsets).expect("DiscId::put failed");
assert_eq!("ByBKvJM1hBL7XtvsPyYtIjlX0Bw-", disc.id());
assert_eq!(3, disc.first_track_num());
assert_eq!(12, disc.last_track_num());
assert_eq!(206535, disc.sectors());
}
#[test]
fn discid_put_too_many_offsets() {
let first = 1;
let offsets: [i32; 101] = [0; 101];
let err = DiscId::put(first, &offsets).expect_err("DiscId::put failed");
assert!(err.to_string().contains("Illegal track limits"));
}
#[test]
fn discid_put_too_many_tracks() {
let first = 11;
let offsets: [i32; 101] = [0; 101];
let err = DiscId::put(first, &offsets).expect_err("DiscId::put failed");
assert!(err.to_string().contains("Illegal track limits"));
}
#[test]
fn discid_parse() {
let toc =
"1 11 242457 150 44942 61305 72755 96360 130485 147315 164275 190702 205412 220437";
let disc = DiscId::parse(toc).expect("DiscId::parse failed");
assert_eq!("lSOVc5h6IXSuzcamJS1Gp4_tRuA-", disc.id());
assert_eq!(toc, disc.toc_string());
}
#[test]
fn discid_parse_minimal() {
let toc = "1 1 44942 150";
let disc = DiscId::parse(toc).expect("DiscId::parse failed");
assert_eq!("ANJa4DGYN_ktpzOwvVPtcjwP7mE-", disc.id());
assert_eq!(toc, disc.toc_string());
}
#[test]
fn discid_parse_first_track_not_one() {
let toc = "3 12 242457 150 18901 39738 59557 79152 100126 124833 147278 166336 182560";
let disc = DiscId::parse(toc).expect("DiscId::parse failed");
assert_eq!("fC1yNbC5bVjbvphqlAY9JyYoWEY-", disc.id());
assert_eq!(toc, disc.toc_string());
}
#[test]
fn discid_parse_invalid_nan() {
let toc = "1 2 242457 150 a";
let err = DiscId::parse(toc).expect_err("DiscId::parse failed");
assert!(err.to_string().contains("invalid digit found in string"));
}
#[test]
fn discid_parse_invalid_too_many_offsets() {
let toc = "1 2 242457 150 200 300";
let err = DiscId::parse(toc).expect_err("DiscId::parse failed");
assert!(
err.to_string()
.contains("TOC string contains too many offsets")
);
}
#[test]
fn discid_parse_invalid_too_many_offsets_total() {
let mut indexes = vec!["0"; 103];
indexes[0] = "1";
indexes[1] = "100";
let toc = indexes.join(" ");
let err = DiscId::parse(&toc).expect_err("DiscId::parse failed");
assert!(
err.to_string()
.contains("TOC string contains too many offsets")
);
}
#[test]
fn discid_parse_invalid_missing_offsets() {
let toc = "1 2 242457 150";
let err = DiscId::parse(toc).expect_err("DiscId::parse failed");
assert!(
err.to_string()
.contains("Number of offsets 1 does not match track count 2")
);
}
#[test]
fn discid_parse_invalid_not_enough_elements() {
let toc = "1";
let err = DiscId::parse(toc).expect_err("DiscId::parse failed");
assert!(err.to_string().contains("Invalid TOC string"));
}
#[test]
fn discid_parse_invalid_empty() {
let toc = "";
let err = DiscId::parse(toc).expect_err("DiscId::parse failed");
assert!(
err.to_string()
.contains("cannot parse integer from empty string")
);
}
#[test]
fn discid_get_track() {
let offsets = [242457, 150, 44942, 61305, 72755];
let disc = DiscId::put(1, &offsets).expect("DiscId::put() failed");
assert!(disc.get_track(0).is_none());
assert!(disc.get_track(1).is_some());
assert!(disc.get_track(4).is_some());
assert!(disc.get_track(5).is_none());
let track = disc.get_track(2).unwrap();
assert_eq!(2, track.number);
assert_eq!(offsets[2], track.offset);
assert_eq!(offsets[3] - offsets[2], track.sectors);
}
#[test]
fn discid_nth_track() {
let first = 1;
let offsets = [
206535, 150, 18901, 39738, 59557, 79152, 100126, 124833, 147278, 166336, 182560,
];
let disc = DiscId::put(first, &offsets).expect("DiscId::put failed");
let track = disc.nth_track(4);
let expected_offset = offsets[4];
let expected_sectors = offsets[5] - offsets[4];
assert_eq!(4, track.number);
assert_eq!("", track.isrc); assert_eq!(
expected_offset, track.offset,
"track {} expected offset {}",
track.number, expected_offset
);
assert_eq!(
expected_sectors, track.sectors,
"track {} expected sectors {}",
track.number, expected_sectors
);
}
#[test]
#[should_panic(expected = "track number out of bounds: 11")]
fn discid_nth_track_out_of_bounds() {
let first = 1;
let offsets = [
206535, 150, 18901, 39738, 59557, 79152, 100126, 124833, 147278, 166336, 182560,
];
let disc = DiscId::put(first, &offsets).expect("DiscId::put failed");
let _ = disc.nth_track(11);
}
#[test]
fn discid_default_device() {
let device = DiscId::default_device();
assert!(!device.is_empty());
}
#[test]
fn discid_has_feature() {
assert_eq!(true, DiscId::has_feature(Features::READ));
}
#[test]
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
fn discid_has_feature_all() {
assert_eq!(true, DiscId::has_feature(Features::READ));
assert_eq!(true, DiscId::has_feature(Features::MCN));
assert_eq!(true, DiscId::has_feature(Features::ISRC));
assert_eq!(true, DiscId::has_feature(Features::all()));
}
#[test]
fn discid_version_string() {
let version = DiscId::version_string();
assert!(version.starts_with("libdiscid"));
}
#[test]
fn discid_debug() {
let first = 1;
let offsets = [2000, 150, 1000];
let disc = DiscId::put(first, &offsets).expect("DiscId::put failed");
assert_eq!("DiscId 1 2 2000 150 1000", format!("{:?}", disc));
}
#[test]
fn features() {
assert_eq!(3, (Features::READ | Features::MCN).bits());
assert_eq!(
Features::all(),
Features::READ | Features::MCN | Features::ISRC
);
}
#[test]
fn track_debug() {
let track = Track {
number: 3,
offset: 57402,
sectors: 32960,
isrc: "DED831801578".to_string(),
};
assert_eq!(
"Track { number: 3, offset: 57402, sectors: 32960, isrc: \"DED831801578\" }",
format!("{:?}", track)
);
}
#[test]
fn disc_error_new() {
let message = "The message";
let error = DiscError::new(message);
assert_eq!(message, error.reason);
}
#[test]
fn disc_error_fmt() {
let error = DiscError::new("The message");
assert_eq!("DiscError: The message", error.to_string());
}
#[test]
fn disc_error_debug() {
let error = DiscError::new("The message");
assert_eq!(
"DiscError { reason: \"The message\" }",
format!("{:?}", error)
);
}
}