#![deny(unsafe_code)]
#![warn(
clippy::filetype_is_file,
clippy::integer_division,
clippy::needless_borrow,
clippy::nursery,
clippy::pedantic,
clippy::perf,
clippy::suboptimal_flops,
clippy::unneeded_field_pattern,
macro_use_extern_crate,
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
non_ascii_idents,
trivial_casts,
trivial_numeric_casts,
unreachable_pub,
unused_crate_dependencies,
unused_extern_crates,
unused_import_braces,
)]
#![allow(
clippy::doc_markdown,
clippy::module_name_repetitions,
)]
#![cfg_attr(feature = "docsrs", feature(doc_cfg))]
mod error;
mod time;
mod track;
#[cfg(feature = "accuraterip")] mod accuraterip;
#[cfg(feature = "cddb")] mod cddb;
#[cfg(feature = "ctdb")] mod ctdb;
#[cfg(feature = "musicbrainz")] mod musicbrainz;
pub use error::TocError;
pub use time::Duration;
pub use track::{
Track,
Tracks,
TrackPosition,
};
#[cfg(feature = "accuraterip")] pub use accuraterip::AccurateRip;
#[cfg(feature = "cddb")] pub use cddb::Cddb;
use std::fmt;
use trimothy::TrimSlice;
const BASE16: [u32; 8] = [1, 16, 256, 4096, 65_536, 1_048_576, 16_777_216, 268_435_456];
const NIL: u8 = u8::MAX;
static UNHEX: [u8; 256] = [
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, 10, 11, 12, 13, 14, 15, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, 10, 11, 12, 13,
14, 15, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL, NIL,
NIL, NIL, NIL,
];
#[cfg(any(feature = "musicbrainz", feature = "ctdb"))]
static ZEROES: [u8; 792] = [b'0'; 792];
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct Toc {
kind: TocKind,
audio: Vec<u32>,
data: u32,
leadout: u32,
}
impl fmt::Display for Toc {
#[cfg(not(feature = "faster-hex"))]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:X}", self.audio.len())?;
for v in &self.audio { write!(f, "+{v:X}")?; }
match self.kind {
TocKind::Audio => write!(f, "+{:X}", self.leadout),
TocKind::CDExtra => write!(f, "+{:X}+{:X}", self.data, self.leadout),
TocKind::DataFirst => write!(f, "+{:X}+X{:X}", self.leadout, self.data),
}
}
#[cfg(feature = "faster-hex")]
#[allow(unsafe_code, clippy::cast_possible_truncation)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use trimothy::TrimSliceMatches;
let mut out = Vec::with_capacity(128);
let mut buf = [b'0'; 8];
let audio_len = self.audio.len() as u8;
faster_hex::hex_encode(&[audio_len], &mut buf[..2]).unwrap();
if 16 <= audio_len { out.push(buf[0]); }
out.push(buf[1]);
macro_rules! push {
($v:expr) => (
faster_hex::hex_encode($v.to_be_bytes().as_slice(), &mut buf).unwrap();
out.push(b'+');
out.extend_from_slice(buf.trim_start_matches(|b| b == b'0'));
);
}
for v in &self.audio { push!(v); }
match self.kind {
TocKind::Audio => { push!(self.leadout); },
TocKind::CDExtra => {
push!(self.data);
push!(self.leadout);
},
TocKind::DataFirst => {
push!(self.leadout);
faster_hex::hex_encode(self.data.to_be_bytes().as_slice(), &mut buf).unwrap();
out.push(b'+');
out.push(b'X');
out.extend_from_slice(buf.trim_start_matches(|b| b == b'0'));
},
}
out.make_ascii_uppercase();
f.write_str(unsafe { std::str::from_utf8_unchecked(&out) })
}
}
impl Toc {
pub fn from_cdtoc<S>(src: S) -> Result<Self, TocError>
where S: AsRef<str> {
let (audio, data, leadout) = parse_cdtoc_metadata(src.as_ref().as_bytes())?;
Self::from_parts(audio, data, leadout)
}
pub fn from_durations<I>(src: I, leadin: Option<u32>) -> Result<Self, TocError>
where I: IntoIterator<Item=Duration> {
let mut last: u32 = leadin.unwrap_or(150);
let mut audio: Vec<u32> = vec![last];
for d in src {
let next = u32::try_from(d.sectors())
.ok()
.and_then(|n| last.checked_add(n))
.ok_or(TocError::SectorSize)?;
audio.push(next);
last = next;
}
let leadout = audio.remove(audio.len() - 1);
Self::from_parts(audio, None, leadout)
}
pub fn from_parts(audio: Vec<u32>, data: Option<u32>, leadout: u32)
-> Result<Self, TocError> {
let audio_len = audio.len();
if 0 == audio_len { return Err(TocError::NoAudio); }
if 99 < audio_len { return Err(TocError::TrackCount); }
if audio[0] < 150 { return Err(TocError::LeadinSize); }
if
(1 < audio_len && audio.windows(2).any(|pair| pair[1] <= pair[0])) ||
leadout <= audio[audio_len - 1]
{
return Err(TocError::SectorOrder);
}
let kind =
if let Some(d) = data {
if d < audio[0] { TocKind::DataFirst }
else if audio[audio_len - 1] < d && d < leadout {
TocKind::CDExtra
}
else { return Err(TocError::SectorOrder); }
}
else { TocKind::Audio };
Ok(Self { kind, audio, data: data.unwrap_or_default(), leadout })
}
pub fn set_audio_leadin(&mut self, leadin: u32) -> Result<(), TocError> {
use std::cmp::Ordering;
if leadin < 150 { Err(TocError::LeadinSize) }
else if matches!(self.kind, TocKind::DataFirst) {
Err(TocError::Format(TocKind::DataFirst))
}
else {
let current = self.audio_leadin();
match leadin.cmp(¤t) {
Ordering::Less => {
let diff = current - leadin;
for v in &mut self.audio { *v -= diff; }
if self.has_data() { self.data -= diff; }
self.leadout -= diff;
},
Ordering::Greater => {
let diff = leadin - current;
for v in &mut self.audio {
*v = v.checked_add(diff).ok_or(TocError::SectorSize)?;
}
if self.has_data() {
self.data = self.data.checked_add(diff)
.ok_or(TocError::SectorSize)?;
}
self.leadout = self.leadout.checked_add(diff)
.ok_or(TocError::SectorSize)?;
},
Ordering::Equal => {},
}
Ok(())
}
}
pub fn set_kind(&mut self, kind: TocKind) -> Result<(), TocError> {
match (self.kind, kind) {
(TocKind::Audio, TocKind::CDExtra) => {
let len = self.audio.len();
if len == 1 { return Err(TocError::NoAudio); }
self.data = self.audio.remove(len - 1);
},
(TocKind::Audio, TocKind::DataFirst) => {
if self.audio.len() == 1 { return Err(TocError::NoAudio); }
self.data = self.audio.remove(0);
},
(TocKind::CDExtra, TocKind::Audio) => {
self.audio.push(self.data);
self.data = 0;
},
(TocKind::DataFirst, TocKind::Audio) => {
self.audio.insert(0, self.data);
self.data = 0;
},
(TocKind::CDExtra, TocKind::DataFirst) => {
self.audio.push(self.data);
self.data = self.audio.remove(0);
},
(TocKind::DataFirst, TocKind::CDExtra) => {
self.audio.insert(0, self.data);
self.data = self.audio.remove(self.audio.len() - 1);
},
_ => return Ok(()),
}
self.kind = kind;
Ok(())
}
}
impl Toc {
#[must_use]
pub fn audio_leadin(&self) -> u32 { self.audio[0] }
#[must_use]
pub const fn audio_leadout(&self) -> u32 {
if matches!(self.kind, TocKind::CDExtra) {
self.data.saturating_sub(11_400)
}
else { self.leadout }
}
#[must_use]
pub fn audio_len(&self) -> usize { self.audio.len() }
#[must_use]
pub fn audio_sectors(&self) -> &[u32] { &self.audio }
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub fn audio_track(&self, num: usize) -> Option<Track> {
let len = self.audio_len();
if num == 0 || len < num { None }
else {
let from = self.audio[num - 1];
let to =
if num < len { self.audio[num] }
else { self.audio_leadout() };
Some(Track {
num: num as u8,
pos: TrackPosition::from((num, len)),
from,
to,
})
}
}
#[must_use]
pub fn audio_tracks(&self) -> Tracks<'_> {
Tracks::new(&self.audio, self.audio_leadout())
}
#[must_use]
pub const fn data_sector(&self) -> Option<u32> {
if self.kind.has_data() { Some(self.data) }
else { None }
}
#[must_use]
pub const fn has_data(&self) -> bool { self.kind.has_data() }
#[must_use]
pub const fn kind(&self) -> TocKind { self.kind }
#[must_use]
pub fn leadin(&self) -> u32 {
if matches!(self.kind, TocKind::DataFirst) { self.data }
else { self.audio[0] }
}
#[must_use]
pub const fn leadout(&self) -> u32 { self.leadout }
#[must_use]
pub fn duration(&self) -> Duration {
Duration::from(self.audio_leadout() - self.audio_leadin())
}
}
#[derive(Debug, Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TocKind {
#[default]
Audio,
CDExtra,
DataFirst,
}
impl fmt::Display for TocKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TocKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Audio => "audio-only",
Self::CDExtra => "CD-Extra",
Self::DataFirst => "data+audio",
}
}
#[must_use]
pub const fn has_data(self) -> bool {
matches!(self, Self::CDExtra | Self::DataFirst)
}
}
#[cfg(feature = "base64")]
#[allow(unsafe_code)]
fn base64_encode(src: &[u8]) -> String {
use base64::{
Engine,
prelude::BASE64_STANDARD,
};
let mut out = String::with_capacity(28);
BASE64_STANDARD.encode_string(src, &mut out);
for b in unsafe { out.as_mut_vec() } {
match *b {
b'+' => { *b = b'.'; },
b'/' => { *b = b'_'; },
b'=' => { *b = b'-'; },
_ => {},
}
}
out
}
#[allow(clippy::cast_lossless)]
#[inline]
fn decode1(src: u8) -> Option<u8> {
let out = UNHEX[src as usize];
if out == NIL { None }
else { Some(out) }
}
fn hex_decode_u8(src: &[u8]) -> Option<u8> {
match src.len() {
1 => decode1(src[0]),
2 => {
let a = decode1(src[0])?;
let b = decode1(src[1])?;
Some(16 * a + b)
},
_ => None,
}
}
fn hex_decode_u32(src: &[u8]) -> Option<u32> {
if (1..=8).contains(&src.len()) {
let mut out: u32 = 0;
for (k, byte) in BASE16.into_iter().zip(src.iter().copied().rev()) {
let digit = u32::from(decode1(byte)?);
out += digit * k;
}
Some(out)
}
else { None }
}
fn parse_cdtoc_metadata(src: &[u8]) -> Result<(Vec<u32>, Option<u32>, u32), TocError> {
let src = src.trim();
let mut split = src.split(|b| b'+'.eq(b));
let audio_len = split.next()
.and_then(hex_decode_u8)
.ok_or(TocError::TrackCount)?;
let sectors: Vec<u32> = split
.by_ref()
.take(usize::from(audio_len))
.map(hex_decode_u32)
.collect::<Option<Vec<u32>>>()
.ok_or(TocError::SectorSize)?;
let sectors_len = sectors.len();
if 0 == sectors_len { return Err(TocError::NoAudio); }
if sectors_len != usize::from(audio_len) {
return Err(TocError::SectorCount(audio_len, sectors_len));
}
let last1 = split.next()
.ok_or(TocError::SectorCount(audio_len, sectors_len - 1))?;
let last1 = hex_decode_u32(last1).ok_or(TocError::SectorSize)?;
if let Some(last2) = split.next() {
let last2 = hex_decode_u32(last2)
.or_else(||
last2.strip_prefix(b"X").or_else(|| last2.strip_prefix(b"x"))
.and_then(hex_decode_u32)
)
.ok_or(TocError::SectorSize)?;
let remaining = split.count();
if remaining == 0 {
if last1 < last2 {
Ok((sectors, Some(last1), last2))
}
else {
Ok((sectors, Some(last2), last1))
}
}
else {
Err(TocError::SectorCount(audio_len, sectors_len + remaining))
}
}
else { Ok((sectors, None, last1)) }
}
#[cfg(test)]
mod tests {
use super::*;
use brunch as _;
const CDTOC_AUDIO: &str = "B+96+5DEF+A0F2+F809+1529F+1ACB3+20CBC+24E14+2AF17+2F4EA+35BDD+3B96D";
const CDTOC_EXTRA: &str = "A+96+3757+696D+C64F+10A13+14DA2+19E88+1DBAA+213A4+2784E+2D7AF+36F11";
const CDTOC_DATA_AUDIO: &str = "A+3757+696D+C64F+10A13+14DA2+19E88+1DBAA+213A4+2784E+2D7AF+36F11+X96";
#[test]
fn t_audio() {
let toc = Toc::from_cdtoc(CDTOC_AUDIO).expect("Unable to parse CDTOC_AUDIO.");
let sectors = vec![
150,
24047,
41202,
63497,
86687,
109747,
134332,
151060,
175895,
193770,
220125,
];
assert_eq!(toc.audio_len(), 11);
assert_eq!(toc.audio_sectors(), §ors);
assert_eq!(toc.data_sector(), None);
assert_eq!(toc.has_data(), false);
assert_eq!(toc.kind(), TocKind::Audio);
assert_eq!(toc.audio_leadin(), 150);
assert_eq!(toc.audio_leadout(), 244077);
assert_eq!(toc.leadin(), 150);
assert_eq!(toc.leadout(), 244077);
assert_eq!(toc.to_string(), CDTOC_AUDIO);
assert_eq!(
Toc::from_parts(sectors, None, 244077),
Ok(toc),
);
let toc = Toc::from_cdtoc("20+96+33BA+5B5E+6C74+7C96+91EE+A9A3+B1AC+BEFC+D2E6+E944+103AC+11426+14B58+174E2+1A9F7+1C794+1F675+21AB9+24090+277DD+2A783+2D508+2DEAA+2F348+31F20+37419+3A463+3DC2F+4064B+43337+4675B+4A7C0")
.expect("Long TOC failed.");
assert_eq!(toc.audio_len(), 32);
assert_eq!(
toc.to_string(),
"20+96+33BA+5B5E+6C74+7C96+91EE+A9A3+B1AC+BEFC+D2E6+E944+103AC+11426+14B58+174E2+1A9F7+1C794+1F675+21AB9+24090+277DD+2A783+2D508+2DEAA+2F348+31F20+37419+3A463+3DC2F+4064B+43337+4675B+4A7C0"
);
let toc = Toc::from_cdtoc("10+96+2B4E+4C51+6B3C+9E08+CD43+FC99+13A55+164B8+191C9+1C0FF+1F613+21B5A+23F70+27A4A+2C20D+2FC65").unwrap();
assert_eq!(toc.audio_len(), 16);
assert_eq!(
toc.to_string(),
"10+96+2B4E+4C51+6B3C+9E08+CD43+FC99+13A55+164B8+191C9+1C0FF+1F613+21B5A+23F70+27A4A+2C20D+2FC65"
);
}
#[test]
fn t_extra() {
let toc = Toc::from_cdtoc(CDTOC_EXTRA).expect("Unable to parse CDTOC_EXTRA.");
let sectors = vec![
150,
14167,
26989,
50767,
68115,
85410,
106120,
121770,
136100,
161870,
];
assert_eq!(toc.audio_len(), 10);
assert_eq!(toc.audio_sectors(), §ors);
assert_eq!(toc.data_sector(), Some(186287));
assert_eq!(toc.has_data(), true);
assert_eq!(toc.kind(), TocKind::CDExtra);
assert_eq!(toc.audio_leadin(), 150);
assert_eq!(toc.audio_leadout(), 174887);
assert_eq!(toc.leadin(), 150);
assert_eq!(toc.leadout(), 225041);
assert_eq!(toc.to_string(), CDTOC_EXTRA);
assert_eq!(
Toc::from_parts(sectors, Some(186287), 225041),
Ok(toc),
);
}
#[test]
fn t_data_first() {
let toc = Toc::from_cdtoc(CDTOC_DATA_AUDIO)
.expect("Unable to parse CDTOC_DATA_AUDIO.");
let sectors = vec![
14167,
26989,
50767,
68115,
85410,
106120,
121770,
136100,
161870,
186287,
];
assert_eq!(toc.audio_len(), 10);
assert_eq!(toc.audio_sectors(), §ors);
assert_eq!(toc.data_sector(), Some(150));
assert_eq!(toc.has_data(), true);
assert_eq!(toc.kind(), TocKind::DataFirst);
assert_eq!(toc.audio_leadin(), 14167);
assert_eq!(toc.audio_leadout(), 225041);
assert_eq!(toc.leadin(), 150);
assert_eq!(toc.leadout(), 225041);
assert_eq!(toc.to_string(), CDTOC_DATA_AUDIO);
assert_eq!(
Toc::from_parts(sectors, Some(150), 225041),
Ok(toc),
);
}
#[test]
fn t_bad() {
for i in [
"A+96+3757+696D+C64F+10A13+14DA2+19E88+1DBAA+213A4+2784E+2D7AF+36F11+36F12",
"A+96+3757+696D+C64F+10A13+14DA2+19E88+1DBAA+213A4+2784E",
"0+96",
"A+96+3757+696D+C64F+10A13+14DA2+19E88+2784E+1DBAA+213A4+2D7AF+36F11",
] {
assert!(Toc::from_cdtoc(i).is_err());
}
}
#[test]
fn t_rekind() {
let mut toc = Toc::from_cdtoc(CDTOC_AUDIO)
.expect("Unable to parse CDTOC_AUDIO.");
assert!(toc.set_kind(TocKind::CDExtra).is_ok());
assert_eq!(toc.audio_len(), 10);
assert_eq!(
toc.audio_sectors(),
&[
150,
24047,
41202,
63497,
86687,
109747,
134332,
151060,
175895,
193770,
]
);
assert_eq!(toc.data_sector(), Some(220125));
assert_eq!(toc.has_data(), true);
assert_eq!(toc.kind(), TocKind::CDExtra);
assert_eq!(toc.audio_leadin(), 150);
assert_eq!(toc.audio_leadout(), 208725);
assert_eq!(toc.leadin(), 150);
assert_eq!(toc.leadout(), 244077);
assert!(toc.set_kind(TocKind::Audio).is_ok());
assert_eq!(Toc::from_cdtoc(CDTOC_AUDIO).unwrap(), toc);
assert!(toc.set_kind(TocKind::DataFirst).is_ok());
assert_eq!(toc.audio_len(), 10);
assert_eq!(
toc.audio_sectors(),
&[
24047,
41202,
63497,
86687,
109747,
134332,
151060,
175895,
193770,
220125,
]
);
assert_eq!(toc.data_sector(), Some(150));
assert_eq!(toc.has_data(), true);
assert_eq!(toc.kind(), TocKind::DataFirst);
assert_eq!(toc.audio_leadin(), 24047);
assert_eq!(toc.audio_leadout(), 244077);
assert_eq!(toc.leadin(), 150);
assert_eq!(toc.leadout(), 244077);
assert!(toc.set_kind(TocKind::Audio).is_ok());
assert_eq!(Toc::from_cdtoc(CDTOC_AUDIO).unwrap(), toc);
toc = Toc::from_cdtoc(CDTOC_EXTRA)
.expect("Unable to parse CDTOC_EXTRA.");
let extra = toc.clone();
let data_audio = Toc::from_cdtoc(CDTOC_DATA_AUDIO)
.expect("Unable to parse CDTOC_DATA_AUDIO.");
assert!(toc.set_kind(TocKind::DataFirst).is_ok());
assert_eq!(toc, data_audio);
assert!(toc.set_kind(TocKind::CDExtra).is_ok());
assert_eq!(toc, extra);
}
#[test]
fn t_unhex8() {
for i in 0..=u8::MAX {
assert_eq!(hex_decode_u8(format!("{:x}", i).as_bytes()), Some(i));
assert_eq!(hex_decode_u8(format!("{:X}", i).as_bytes()), Some(i));
assert_eq!(hex_decode_u8(format!("{:02x}", i).as_bytes()), Some(i));
assert_eq!(hex_decode_u8(format!("{:02X}", i).as_bytes()), Some(i));
}
}
#[test]
#[ignore = "(very long-running)"]
fn t_unhexu32() {
for i in 0..=u32::MAX {
assert_eq!(hex_decode_u32(format!("{:x}", i).as_bytes()), Some(i));
assert_eq!(hex_decode_u32(format!("{:X}", i).as_bytes()), Some(i));
assert_eq!(hex_decode_u32(format!("{:08x}", i).as_bytes()), Some(i));
assert_eq!(hex_decode_u32(format!("{:08X}", i).as_bytes()), Some(i));
}
}
}