#![deny(
clippy::allow_attributes_without_reason,
clippy::correctness,
unreachable_pub,
unsafe_code,
)]
#![warn(
clippy::complexity,
clippy::nursery,
clippy::pedantic,
clippy::perf,
clippy::style,
clippy::allow_attributes,
clippy::clone_on_ref_ptr,
clippy::create_dir,
clippy::filetype_is_file,
clippy::format_push_string,
clippy::get_unwrap,
clippy::impl_trait_in_params,
clippy::implicit_clone,
clippy::lossy_float_literal,
clippy::missing_assert_message,
clippy::missing_docs_in_private_items,
clippy::needless_raw_strings,
clippy::panic_in_result_fn,
clippy::pub_without_shorthand,
clippy::rest_pat_in_fully_bound_structs,
clippy::semicolon_inside_block,
clippy::str_to_string,
clippy::todo,
clippy::undocumented_unsafe_blocks,
clippy::unneeded_field_pattern,
clippy::unseparated_literal_suffix,
clippy::unwrap_in_result,
macro_use_extern_crate,
missing_copy_implementations,
missing_docs,
non_ascii_idents,
trivial_casts,
trivial_numeric_casts,
unused_crate_dependencies,
unused_extern_crates,
unused_import_braces,
)]
#![expect(clippy::doc_markdown, reason = "This gets annoying with names like MusicBrainz.")]
#![cfg_attr(docsrs, feature(doc_cfg))]
mod error;
mod hex;
mod time;
mod track;
#[cfg(feature = "accuraterip")] mod accuraterip;
#[cfg(feature = "cddb")] mod cddb;
#[cfg(feature = "ctdb")] mod ctdb;
#[cfg(feature = "musicbrainz")] mod musicbrainz;
#[cfg(feature = "serde")] mod serde;
#[cfg(feature = "sha1")] mod shab64;
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;
#[cfg(feature = "sha1")] pub use shab64::ShaB64;
use dactyl::traits::HexToUnsigned;
use std::fmt;
#[cfg(any(feature = "ctdb", feature = "musicbrainz"))]
const TRACK_ZEROES: [[u8; 8]; 100] = [[b'0'; 8]; 100];
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
pub struct Toc {
kind: TocKind,
audio: Vec<u32>,
data: u32,
leadout: u32,
}
impl fmt::Display for Toc {
#[expect(clippy::cast_possible_truncation, reason = "False positive.")]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const fn trim_leading_zeroes(mut src: &[u8]) -> &[u8] {
while let [b'0', rest @ ..] = src { src = rest; }
src
}
let mut out = Vec::with_capacity(128);
let audio_len = self.audio.len() as u8;
let buf = hex::upper_encode_u8(audio_len);
if 16 <= audio_len { out.push(buf[0]); }
out.push(buf[1]);
macro_rules! push {
($v:expr) => (
out.push(b'+');
out.extend_from_slice(trim_leading_zeroes(&hex::upper_encode_u32($v)));
);
}
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);
out.push(b'+');
out.push(b'X');
out.extend_from_slice(trim_leading_zeroes(&hex::upper_encode_u32(self.data)));
},
}
std::str::from_utf8(&out)
.map_err(|_| fmt::Error)
.and_then(|s| <str as fmt::Display>::fmt(s, f))
}
}
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.array_windows().any(|[a, b]| a >= b)) ||
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 const fn audio_leadin(&self) -> u32 {
if let [ out, .. ] = self.audio.as_slice() { *out }
else { 150 }
}
#[must_use]
pub const fn audio_leadin_normalized(&self) -> u32 { self.audio_leadin() - 150 }
#[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 const fn audio_leadout_normalized(&self) -> u32 {
self.audio_leadout() - 150
}
#[must_use]
pub const fn audio_len(&self) -> usize { self.audio.len() }
#[must_use]
pub const fn audio_sectors(&self) -> &[u32] { self.audio.as_slice() }
#[expect(clippy::cast_possible_truncation, reason = "False positive.")]
#[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 const fn audio_tracks(&self) -> Tracks<'_> {
Tracks::new(self.audio.as_slice(), 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 data_sector_normalized(&self) -> Option<u32> {
if self.kind.has_data() { Some(self.data.saturating_sub(150)) }
else { None }
}
#[must_use]
pub const fn has_data(&self) -> bool { self.kind.has_data() }
#[must_use]
pub const fn htoa(&self) -> Option<Track> {
let leadin = self.audio_leadin();
if leadin == 150 || matches!(self.kind, TocKind::DataFirst) { None }
else {
Some(Track {
num: 0,
pos: TrackPosition::Invalid,
from: 150,
to: leadin,
})
}
}
#[must_use]
pub const fn kind(&self) -> TocKind { self.kind }
#[must_use]
pub const fn leadin(&self) -> u32 {
if matches!(self.kind, TocKind::DataFirst) { self.data }
else { self.audio_leadin() }
}
#[must_use]
pub const fn leadin_normalized(&self) -> u32 {
self.leadin().saturating_sub(150)
}
#[must_use]
pub const fn leadout(&self) -> u32 { self.leadout }
#[must_use]
pub const fn leadout_normalized(&self) -> u32 { self.leadout - 150 }
#[must_use]
pub const fn duration(&self) -> Duration {
Duration((self.audio_leadout() - self.audio_leadin()) as u64)
}
}
#[derive(Debug, Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum TocKind {
#[default]
Audio,
CDExtra,
DataFirst,
}
impl fmt::Display for TocKind {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<str as fmt::Display>::fmt(self.as_str(), f)
}
}
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)
}
}
fn parse_cdtoc_metadata(src: &[u8]) -> Result<(Vec<u32>, Option<u32>, u32), TocError> {
let src = src.trim_ascii();
let mut split = src.split(|b| b'+'.eq(b));
let audio_len = split.next()
.and_then(u8::htou)
.ok_or(TocError::TrackCount)?;
let sectors: Vec<u32> = split
.by_ref()
.take(usize::from(audio_len))
.map(u32::htou)
.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 = u32::htou(last1).ok_or(TocError::SectorSize)?;
if let Some(last2) = split.next() {
let last2 = u32::htou(last2)
.or_else(||
last2.strip_prefix(b"X").or_else(|| last2.strip_prefix(b"x"))
.and_then(u32::htou)
)
.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 _;
use serde_json 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,
109_747,
134_332,
151_060,
175_895,
193_770,
220_125,
];
assert_eq!(toc.audio_len(), 11);
assert_eq!(toc.audio_sectors(), §ors);
assert_eq!(toc.data_sector(), None);
assert!(!toc.has_data());
assert_eq!(toc.kind(), TocKind::Audio);
assert_eq!(toc.audio_leadin(), 150);
assert_eq!(toc.audio_leadout(), 244_077);
assert_eq!(toc.leadin(), 150);
assert_eq!(toc.leadout(), 244_077);
assert_eq!(toc.to_string(), CDTOC_AUDIO);
assert_eq!(
Toc::from_parts(sectors, None, 244_077),
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,
106_120,
121_770,
136_100,
161_870,
];
assert_eq!(toc.audio_len(), 10);
assert_eq!(toc.audio_sectors(), §ors);
assert_eq!(toc.data_sector(), Some(186_287));
assert!(toc.has_data());
assert_eq!(toc.kind(), TocKind::CDExtra);
assert_eq!(toc.audio_leadin(), 150);
assert_eq!(toc.audio_leadout(), 174_887);
assert_eq!(toc.leadin(), 150);
assert_eq!(toc.leadout(), 225_041);
assert_eq!(toc.to_string(), CDTOC_EXTRA);
assert_eq!(
Toc::from_parts(sectors, Some(186_287), 225_041),
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,
106_120,
121_770,
136_100,
161_870,
186_287,
];
assert_eq!(toc.audio_len(), 10);
assert_eq!(toc.audio_sectors(), §ors);
assert_eq!(toc.data_sector(), Some(150));
assert!(toc.has_data());
assert_eq!(toc.kind(), TocKind::DataFirst);
assert_eq!(toc.audio_leadin(), 14167);
assert_eq!(toc.audio_leadout(), 225_041);
assert_eq!(toc.leadin(), 150);
assert_eq!(toc.leadout(), 225_041);
assert_eq!(toc.to_string(), CDTOC_DATA_AUDIO);
assert_eq!(
Toc::from_parts(sectors, Some(150), 225_041),
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]
#[expect(clippy::cognitive_complexity, reason = "It is what it is.")]
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,
109_747,
134_332,
151_060,
175_895,
193_770,
]
);
assert_eq!(toc.data_sector(), Some(220_125));
assert!(toc.has_data());
assert_eq!(toc.kind(), TocKind::CDExtra);
assert_eq!(toc.audio_leadin(), 150);
assert_eq!(toc.audio_leadout(), 208_725);
assert_eq!(toc.leadin(), 150);
assert_eq!(toc.leadout(), 244_077);
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,
109_747,
134_332,
151_060,
175_895,
193_770,
220_125,
]
);
assert_eq!(toc.data_sector(), Some(150));
assert!(toc.has_data());
assert_eq!(toc.kind(), TocKind::DataFirst);
assert_eq!(toc.audio_leadin(), 24047);
assert_eq!(toc.audio_leadout(), 244_077);
assert_eq!(toc.leadin(), 150);
assert_eq!(toc.leadout(), 244_077);
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);
}
}