cdtoc 0.1.5

Parser and tools for CDTOC metadata tags.
Documentation
/*!
# CDTOC: AccurateRip
*/

use crate::{
	Cddb,
	Toc,
	TocError,
};
use std::{
	collections::BTreeMap,
	fmt,
};



#[cfg_attr(feature = "docsrs", doc(cfg(feature = "accuraterip")))]
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
/// # AccurateRip ID.
///
/// This struct holds an [AccurateRip](http://accuraterip.com/) ID.
///
/// Values of this type are returned by [`Toc::accuraterip_id`].
///
/// ## Examples
///
/// ```
/// use cdtoc::Toc;
///
/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
/// let ar_id = toc.accuraterip_id();
///
/// // Usually you'll want this value as a string:
/// assert_eq!(
///     ar_id.to_string(),
///     "004-0002189a-00087f33-1f02e004",
/// );
///
/// // But you can also get a binary version matching the format of the
/// // checksum bin files:
/// assert_eq!(
///     <[u8; 13]>::from(ar_id),
///     [4, 154, 24, 2, 0, 51, 127, 8, 0, 4, 224, 2, 31],
/// );
/// ```
pub struct AccurateRip([u8; 13]);

impl AsRef<[u8]> for AccurateRip {
	fn as_ref(&self) -> &[u8] { &self.0 }
}

impl From<AccurateRip> for [u8; 13] {
	fn from(src: AccurateRip) -> Self { src.0 }
}

impl fmt::Display for AccurateRip {
	#[cfg(feature = "faster-hex")]
	#[allow(unsafe_code)]
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		f.write_str(&self.pretty_print())
	}

	#[cfg(not(feature = "faster-hex"))]
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		let b = u32::from_le_bytes([self.0[1], self.0[2], self.0[3], self.0[4]]);
		let c = u32::from_le_bytes([self.0[5], self.0[6], self.0[7], self.0[8]]);
		let d = u32::from_le_bytes([self.0[9], self.0[10], self.0[11], self.0[12]]);

		write!(f, "{:03}-{b:08x}-{c:08x}-{d:08x}", self.0[0])
	}
}

impl From<&Toc> for AccurateRip {
	#[allow(clippy::cast_possible_truncation)]
	fn from(src: &Toc) -> Self {
		let mut b: u32 = 0;
		let mut c: u32 = 0;

		let mut idx = 1;
		for v in src.audio_sectors() {
			let off = v.saturating_sub(150);
			b += off;
			c += off.max(1) * idx;
			idx += 1;
		}

		// Add in the last part.
		let leadout = src.leadout().saturating_sub(150);

		let b = (b + leadout).to_le_bytes();
		let c = (c + leadout.max(1) * idx).to_le_bytes();
		let d = u32::from(src.cddb_id()).to_le_bytes();

		Self([
			src.audio_len() as u8,
			b[0], b[1], b[2], b[3],
			c[0], c[1], c[2], c[3],
			d[0], d[1], d[2], d[3],
		])
	}
}

impl AccurateRip {
	#[cfg_attr(feature = "docsrs", doc(cfg(feature = "accuraterip")))]
	#[must_use]
	/// # Number of Audio Tracks.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// // From Toc.
	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
	/// assert_eq!(toc.audio_len(), 4_usize);
	///
	/// // From AccurateRip.
	/// let disc_id = toc.accuraterip_id();
	/// assert_eq!(disc_id.audio_len(), 4_u8);
	/// ```
	pub const fn audio_len(&self) -> u8 { self.0[0] }

	#[cfg_attr(feature = "docsrs", doc(cfg(feature = "accuraterip")))]
	#[must_use]
	/// # AccurateRip Checksum URL.
	///
	/// This returns the URL where you can download the v1 and v2 checksums for
	/// the disc, provided it is actually _in_ the AccurateRip database. (If it
	/// isn't, their server will return a `404`.)
	///
	/// You can also get this directly via [`Toc::accuraterip_checksum_url`].
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
	/// let ar_id = toc.accuraterip_id();
	/// assert_eq!(
	///     ar_id.checksum_url(),
	///     "http://www.accuraterip.com/accuraterip/a/9/8/dBAR-004-0002189a-00087f33-1f02e004.bin",
	/// );
	/// ```
	pub fn checksum_url(&self) -> String {
		let disc_id = self.to_string();
		[
			"http://www.accuraterip.com/accuraterip/",
			&disc_id[11..12],
			"/",
			&disc_id[10..11],
			"/",
			&disc_id[9..10],
			"/dBAR-",
			&disc_id,
			".bin",
		].concat()
	}

	#[cfg_attr(feature = "docsrs", doc(cfg(all(feature = "accuraterip", feature = "cddb"))))]
	#[must_use]
	/// # CDDB ID.
	///
	/// In cases where your application requires both AccurateRip and CDDB IDs,
	/// using this method to obtain the latter is cheaper than calling
	/// [`Toc::cddb_id`].
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
	/// let ar_id = toc.accuraterip_id();
	/// assert_eq!(
	///     ar_id.cddb_id(),
	///     toc.cddb_id(),
	/// );
	/// ```
	pub const fn cddb_id(&self) -> Cddb {
		Cddb(u32::from_le_bytes([
			self.0[9],
			self.0[10],
			self.0[11],
			self.0[12],
		]))
	}

	#[cfg_attr(feature = "docsrs", doc(cfg(feature = "accuraterip")))]
	/// # Parse Checksums.
	///
	/// This will parse the v1 and v2 track checksums from a raw AccurateRip
	/// [bin file](AccurateRip::checksum_url).
	///
	/// The return result is a vector — indexed by track number (`n-1`) — of
	/// `checksum => confidence` pairs.
	///
	/// Note: AccurateRip does not differentiate between v1 and v2 checksums;
	/// the only way to know which is which is to find a match for a checksum
	/// you calculated yourself.
	///
	/// ## Errors
	///
	/// This will return an error if parsing is unsuccessful, or the result is
	/// empty.
	pub fn parse_checksums(&self, bin: &[u8]) -> Result<Vec<BTreeMap<u32, u8>>, TocError> {
		// We're expecting 0+ sections containing a 13-byte disc ID and a
		// 9-byte checksum for each track.
		let audio_len = self.audio_len() as usize;
		let chunk_size = 13 + 9 * audio_len;
		let mut out: Vec<BTreeMap<u32, u8>> = vec![BTreeMap::default(); audio_len];

		for chunk in bin.chunks_exact(chunk_size) {
			// Verify the chunk begins with the disc ID, and get to the meat.
			let chunk = chunk.strip_prefix(&self.0).ok_or(TocError::Checksums)?;
			// Update the list for each track, combining them if for some
			// reason the same value appears twice.
			for (k, v) in chunk.chunks_exact(9).enumerate() {
				let crc = u32::from_le_bytes([v[1], v[2], v[3], v[4]]);
				if crc != 0 {
					let e = out[k].entry(crc).or_insert(0);
					*e = e.saturating_add(v[0]);
				}
			}
		}

		// Consider it okay if we found at least one checksum.
		if out.iter().any(|v| ! v.is_empty()) { Ok(out) }
		else { Err(TocError::NoChecksums) }
	}

	#[cfg_attr(feature = "docsrs", doc(cfg(all(feature = "accuraterip", feature = "faster-hex"))))]
	#[cfg(feature = "faster-hex")]
	#[allow(unsafe_code, clippy::missing_panics_doc)]
	#[must_use]
	/// # Pretty Print.
	///
	/// Return a String representation of the disc ID, same as `[AccurateRip::to_string]`,
	/// but a little faster.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("D+96+4FFB+7F76+BB0A+EF38+12FB3+16134+1BCC4+1EC21+24A6A+272F9+299FA+2CCA6+30EE6").unwrap();
	/// assert_eq!(
	///     toc.accuraterip_id().pretty_print(),
	///     "013-0015deca-00d9b921-9a0a6e0d",
	/// );
	/// ```
	pub fn pretty_print(&self) -> String {
		let mut out: Vec<u8> = vec![
			b'0', b'0', b'0',
			b'-', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0',
			b'-', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0',
			b'-', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0',
		];

		// Length.
		out[..3].copy_from_slice(dactyl::NiceU8::from(self.0[0]).as_bytes3());

		// ID Parts.
		faster_hex::hex_encode(&[self.0[4], self.0[3], self.0[2], self.0[1]], &mut out[4..12]).unwrap();
		faster_hex::hex_encode(&[self.0[8], self.0[7], self.0[6], self.0[5]], &mut out[13..21]).unwrap();
		faster_hex::hex_encode(&[self.0[12], self.0[11], self.0[10], self.0[9]], &mut out[22..]).unwrap();

		// Safety: all bytes are ASCII.
		unsafe { String::from_utf8_unchecked(out) }
	}
}



impl Toc {
	#[cfg_attr(feature = "docsrs", doc(cfg(feature = "accuraterip")))]
	#[must_use]
	/// # AccurateRip ID.
	///
	/// This returns the [AccurateRip](http://accuraterip.com/) ID
	/// corresponding to the table of contents.
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
	/// let ar_id = toc.accuraterip_id();
	///
	/// // Usually you'll want this value as a string:
	/// assert_eq!(
	///     ar_id.to_string(),
	///     "004-0002189a-00087f33-1f02e004",
	/// );
	///
	/// // But you can also get a binary version matching the format of the
	/// // checksum bin files:
	/// assert_eq!(
	///     <[u8; 13]>::from(ar_id),
	///     [4, 154, 24, 2, 0, 51, 127, 8, 0, 4, 224, 2, 31],
	/// );
	/// ```
	pub fn accuraterip_id(&self) -> AccurateRip { AccurateRip::from(self) }

	#[cfg_attr(feature = "docsrs", doc(cfg(feature = "accuraterip")))]
	#[must_use]
	/// # AccurateRip Checksum URL.
	///
	/// This returns the URL where you can download the v1 and v2 checksums for
	/// the disc, provided it is actually _in_ the AccurateRip database. (If it
	/// isn't, their server will return a `404`.)
	///
	/// ## Examples
	///
	/// ```
	/// use cdtoc::Toc;
	///
	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
	/// assert_eq!(
	///     toc.accuraterip_checksum_url(),
	///     "http://www.accuraterip.com/accuraterip/a/9/8/dBAR-004-0002189a-00087f33-1f02e004.bin",
	/// );
	/// ```
	pub fn accuraterip_checksum_url(&self) -> String {
		self.accuraterip_id().checksum_url()
	}

	#[cfg_attr(feature = "docsrs", doc(cfg(feature = "accuraterip")))]
	/// # Parse Checksums.
	///
	/// This will parse the v1 and v2 track checksums from a raw AccurateRip
	/// [bin file](AccurateRip::checksum_url).
	///
	/// See [`AccurateRip::parse_checksums`] for more information.
	///
	/// ## Errors
	///
	/// This will return an error if parsing is unsuccessful, or the result is
	/// empty.
	pub fn accuraterip_parse_checksums(&self, bin: &[u8]) -> Result<Vec<BTreeMap<u32, u8>>, TocError> {
		self.accuraterip_id().parse_checksums(bin)
	}
}



#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn t_accuraterip() {
		for (t, c) in [
			(
				"D+96+3B5D+78E3+B441+EC83+134F4+17225+1A801+1EA5C+23B5B+27CEF+2B58B+2F974+35D56+514C8",
				"013-001802ed-00f8ee31-b611560e",
			),
			(
				"4+96+2D2B+6256+B327+D84A",
				"004-0002189a-00087f33-1f02e004",
			),
			(
				"10+B6+5352+62AC+99D6+E218+12AC0+135E7+142E9+178B0+19D22+1B0D0+1E7FA+22882+247DB+27074+2A1BD+2C0FB",
				"016-0018be61-012232a8-d6096410",
			),
			(
				"15+247E+2BEC+4AF4+7368+9704+B794+E271+110D0+12B7A+145C1+16CAF+195CF+1B40F+1F04A+21380+2362D+2589D+2793D+2A760+2DA32+300E1+32B46",
				"021-0022250d-020afc1b-100a5515",
			),
			(
				"63+96+12D9+5546+A8A2+CAAA+128BF+17194+171DF+1722A+17275+172C0+1730B+17356+173A1+173EC+17437+17482+174CD+17518+17563+175AE+175F9+17644+1768F+176DA+17725+17770+177BB+17806+17851+1789C+178E7+17932+1797D+179C8+17A13+17A5E+17AA9+17AF4+17B3F+17B8A+17BD5+17C20+17C6B+17CB6+17D01+17D4C+17D97+17DE2+17E2D+17E78+17EC3+17F0E+17F59+17FA4+17FEF+1803A+18085+180D0+1811B+18166+181B1+181FC+18247+18292+182DD+18328+18373+183BE+18409+18454+1849F+184EA+18535+18580+185CB+18616+18661+186AC+186F7+18742+1878D+187D8+18823+1886E+188B9+18904+1894F+1899A+189E5+18A30+18A7B+18AC6+18B11+18B5C+18BA7+18BF2+18C38+1ECDC+246E9",
				"099-00909976-1e2814f1-cc07c363",
			),
		] {
			let toc = Toc::from_cdtoc(t).expect("Invalid TOC");
			assert_eq!(toc.accuraterip_id().to_string(), c);
		}
	}
}