1use crate::{
6 Cddb,
7 Toc,
8 TocError,
9};
10use dactyl::traits::{
11 BytesToUnsigned,
12 HexToUnsigned,
13};
14use std::{
15 collections::BTreeMap,
16 fmt,
17 ops::Range,
18 str::FromStr,
19};
20
21
22
23const DRIVE_OFFSET_VENDOR_MAX: usize = 8;
27
28const DRIVE_OFFSET_MODEL_MAX: usize = 16;
32
33const DRIVE_OFFSET_OFFSET_RNG: Range<i16> = -2940..2941;
38
39
40
41#[cfg_attr(docsrs, doc(cfg(feature = "accuraterip")))]
42#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
43pub struct AccurateRip([u8; 13]);
71
72impl AsRef<[u8]> for AccurateRip {
73 #[inline]
74 fn as_ref(&self) -> &[u8] { self.0.as_slice() }
75}
76
77impl From<AccurateRip> for [u8; 13] {
78 #[inline]
79 fn from(src: AccurateRip) -> Self { src.0 }
80}
81
82impl fmt::Display for AccurateRip {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 let disc_id = self.encode();
85 std::str::from_utf8(disc_id.as_slice())
86 .map_err(|_| fmt::Error)
87 .and_then(|s| <str as fmt::Display>::fmt(s, f))
88 }
89}
90
91impl From<&Toc> for AccurateRip {
92 #[expect(clippy::cast_possible_truncation, reason = "False positive.")]
93 fn from(src: &Toc) -> Self {
94 let mut b: u32 = 0;
95 let mut c: u32 = 0;
96
97 let mut idx = 1;
98 for v in src.audio_sectors() {
99 let off = v.saturating_sub(150);
100 b += off;
101 c += off.max(1) * idx;
102 idx += 1;
103 }
104
105 let leadout = src.leadout().saturating_sub(150);
107
108 let b = (b + leadout).to_le_bytes();
109 let c = (c + leadout.max(1) * idx).to_le_bytes();
110 let d = u32::from(src.cddb_id()).to_le_bytes();
111
112 Self([
113 src.audio_len() as u8,
114 b[0], b[1], b[2], b[3],
115 c[0], c[1], c[2], c[3],
116 d[0], d[1], d[2], d[3],
117 ])
118 }
119}
120
121impl FromStr for AccurateRip {
122 type Err = TocError;
123 #[inline]
124 fn from_str(src: &str) -> Result<Self, Self::Err> { Self::decode(src) }
125}
126
127impl TryFrom<&str> for AccurateRip {
128 type Error = TocError;
129 #[inline]
130 fn try_from(src: &str) -> Result<Self, Self::Error> { Self::decode(src) }
131}
132
133impl AccurateRip {
134 pub const DRIVE_OFFSET_URL: &'static str = "http://www.accuraterip.com/accuraterip/DriveOffsets.bin";
142}
143
144impl AccurateRip {
145 #[must_use]
146 pub const fn audio_len(&self) -> u8 { self.0[0] }
162
163 #[expect(unsafe_code, reason = "For performance.")]
164 #[must_use]
165 pub fn checksum_url(&self) -> String {
186 let disc_id = self.encode();
188 debug_assert!(disc_id.is_ascii(), "Bug: AccurateRip ID is not ASCII?!");
189
190 let mut out = String::with_capacity(84);
191 out.push_str("http://www.accuraterip.com/accuraterip/");
192 out.push(char::from(disc_id[11]));
193 out.push('/');
194 out.push(char::from(disc_id[10]));
195 out.push('/');
196 out.push(char::from(disc_id[9]));
197 out.push_str("/dBAR-");
198 out.push_str(unsafe { std::str::from_utf8_unchecked(disc_id.as_slice()) });
200 out.push_str(".bin");
201 out
202 }
203
204 #[must_use]
205 pub const fn cddb_id(&self) -> Cddb {
224 Cddb(u32::from_le_bytes([
225 self.0[9],
226 self.0[10],
227 self.0[11],
228 self.0[12],
229 ]))
230 }
231
232 pub fn decode<S>(src: S) -> Result<Self, TocError>
264 where S: AsRef<str> {
265 let src = src.as_ref().as_bytes();
266 if src.len() == 30 && src[3] == b'-' && src[12] == b'-' && src[21] == b'-' {
267 let a = u8::btou(&src[..3]).ok_or(TocError::AccurateRipDecode)?;
268 let b = u32::htou(&src[4..12])
269 .map(u32::to_le_bytes)
270 .ok_or(TocError::AccurateRipDecode)?;
271 let c = u32::htou(&src[13..21])
272 .map(u32::to_le_bytes)
273 .ok_or(TocError::AccurateRipDecode)?;
274 let d = u32::htou(&src[22..])
275 .map(u32::to_le_bytes)
276 .ok_or(TocError::AccurateRipDecode)?;
277
278 Ok(Self([
279 a,
280 b[0], b[1], b[2], b[3],
281 c[0], c[1], c[2], c[3],
282 d[0], d[1], d[2], d[3],
283 ]))
284 }
285 else { Err(TocError::AccurateRipDecode) }
286 }
287
288 pub fn parse_checksums(&self, bin: &[u8]) -> Result<Vec<BTreeMap<u32, u8>>, TocError> {
305 let audio_len = self.audio_len() as usize;
308 let chunk_size = 13 + 9 * audio_len;
309 let mut out: Vec<BTreeMap<u32, u8>> = vec![BTreeMap::default(); audio_len];
310
311 for chunk in bin.chunks_exact(chunk_size) {
312 let chunk = chunk.strip_prefix(&self.0).ok_or(TocError::Checksums)?;
314 for (k, v) in chunk.chunks_exact(9).enumerate() {
317 let crc = u32::from_le_bytes([v[1], v[2], v[3], v[4]]);
318 if crc != 0 {
319 let e = out[k].entry(crc).or_insert(0);
320 *e = e.saturating_add(v[0]);
321 }
322 }
323 }
324
325 if out.iter().any(|v| ! v.is_empty()) { Ok(out) }
327 else { Err(TocError::NoChecksums) }
328 }
329
330 pub fn parse_drive_offsets(raw: &[u8])
344 -> Result<BTreeMap<(&str, &str), i16>, TocError> {
345 const BLOCK_SIZE: usize = 69;
349
350 const fn trim_vm(c: char) -> bool { c.is_ascii_whitespace() || c.is_ascii_control() }
355
356 if raw.len() < BLOCK_SIZE { return Err(TocError::NoDriveOffsets); }
358
359 let mut out = BTreeMap::default();
363 for chunk in raw.chunks_exact(BLOCK_SIZE) {
364 let offset = i16::from_le_bytes([chunk[0], chunk[1]]);
366
367 let vm = std::str::from_utf8(&chunk[2..34])
370 .ok()
371 .filter(|vm| vm.is_ascii())
372 .ok_or(TocError::DriveOffsetDecode)?;
373
374 let (vendor, model) =
375 if let Some(model) = vm.strip_prefix("- ") {
377 ("", model.trim_matches(trim_vm))
378 }
379 else {
382 let mut split = vm.splitn(2, " - ");
383 let vendor = split.next().ok_or(TocError::DriveOffsetDecode)?;
384 let model = split.next().unwrap_or("");
385 (vendor.trim_matches(trim_vm), model.trim_matches(trim_vm))
386 };
387
388 if model.is_empty() {}
390 else if
392 DRIVE_OFFSET_OFFSET_RNG.contains(&offset) &&
393 vendor.len() <= DRIVE_OFFSET_VENDOR_MAX &&
394 model.len() <= DRIVE_OFFSET_MODEL_MAX &&
395 vendor.is_ascii() && model.is_ascii()
396 {
397 out.insert((vendor, model), offset);
398 }
399 else { return Err(TocError::DriveOffsetDecode); }
401 }
402
403 if out.is_empty() { Err(TocError::NoDriveOffsets) }
405 else { Ok(out) }
406 }
407}
408
409impl AccurateRip {
410 #[inline]
411 fn encode(&self) -> [u8; 30] {
416 let mut disc_id: [u8; 30] = [
417 b'0', b'0', b'0',
418 b'-', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0',
419 b'-', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0',
420 b'-', b'0', b'0', b'0', b'0', b'0', b'0', b'0', b'0',
421 ];
422
423 disc_id[..3].copy_from_slice(dactyl::NiceU8::from(self.0[0]).as_bytes3());
425
426 faster_hex::hex_encode_fallback(&[self.0[4], self.0[3], self.0[2], self.0[1]], &mut disc_id[4..12]);
428 faster_hex::hex_encode_fallback(&[self.0[8], self.0[7], self.0[6], self.0[5]], &mut disc_id[13..21]);
429 faster_hex::hex_encode_fallback(&[self.0[12], self.0[11], self.0[10], self.0[9]], &mut disc_id[22..]);
430
431 disc_id
432 }
433}
434
435
436
437impl Toc {
438 #[cfg_attr(docsrs, doc(cfg(feature = "accuraterip")))]
439 #[must_use]
440 pub fn accuraterip_id(&self) -> AccurateRip { AccurateRip::from(self) }
467
468 #[cfg_attr(docsrs, doc(cfg(feature = "accuraterip")))]
469 #[must_use]
470 pub fn accuraterip_checksum_url(&self) -> String {
488 self.accuraterip_id().checksum_url()
489 }
490
491 #[cfg_attr(docsrs, doc(cfg(feature = "accuraterip")))]
492 pub fn accuraterip_parse_checksums(&self, bin: &[u8]) -> Result<Vec<BTreeMap<u32, u8>>, TocError> {
504 self.accuraterip_id().parse_checksums(bin)
505 }
506}
507
508
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 const OFFSET_BIN: &[u8] = &[155, 2, 80, 73, 79, 78, 69, 69, 82, 32, 32, 45, 32, 66, 68, 45, 82, 87, 32, 32, 32, 66, 68, 82, 45, 88, 49, 50, 0, 0, 0, 0, 0, 0, 0, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 155, 2, 80, 73, 79, 78, 69, 69, 82, 32, 32, 45, 32, 66, 68, 45, 82, 87, 32, 32, 32, 66, 68, 82, 45, 88, 49, 50, 85, 0, 0, 0, 0, 0, 0, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 155, 2, 80, 73, 79, 78, 69, 69, 82, 32, 32, 45, 32, 66, 68, 45, 82, 87, 32, 32, 32, 66, 68, 82, 45, 88, 49, 51, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 155, 2, 80, 73, 79, 78, 69, 69, 82, 32, 32, 45, 32, 66, 68, 45, 82, 87, 32, 32, 32, 66, 68, 82, 45, 88, 49, 51, 85, 0, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
516
517 #[test]
518 fn t_accuraterip() {
519 for (t, id) in [
520 (
521 "D+96+3B5D+78E3+B441+EC83+134F4+17225+1A801+1EA5C+23B5B+27CEF+2B58B+2F974+35D56+514C8",
522 "013-001802ed-00f8ee31-b611560e",
523 ),
524 (
525 "4+96+2D2B+6256+B327+D84A",
526 "004-0002189a-00087f33-1f02e004",
527 ),
528 (
529 "10+B6+5352+62AC+99D6+E218+12AC0+135E7+142E9+178B0+19D22+1B0D0+1E7FA+22882+247DB+27074+2A1BD+2C0FB",
530 "016-0018be61-012232a8-d6096410",
531 ),
532 (
533 "15+247E+2BEC+4AF4+7368+9704+B794+E271+110D0+12B7A+145C1+16CAF+195CF+1B40F+1F04A+21380+2362D+2589D+2793D+2A760+2DA32+300E1+32B46",
534 "021-0022250d-020afc1b-100a5515",
535 ),
536 (
537 "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",
538 "099-00909976-1e2814f1-cc07c363",
539 ),
540 ] {
541 let toc = Toc::from_cdtoc(t).expect("Invalid TOC");
542 let ar_id = toc.accuraterip_id();
543 assert_eq!(ar_id.to_string(), id);
544
545 assert_eq!(AccurateRip::decode(id), Ok(ar_id));
547 assert_eq!(AccurateRip::try_from(id), Ok(ar_id));
548 assert_eq!(id.parse::<AccurateRip>(), Ok(ar_id));
549 }
550 }
551
552 #[test]
553 fn t_drive_offsets() {
554 let parsed = AccurateRip::parse_drive_offsets(OFFSET_BIN)
555 .expect("Drive offset parsing failed.");
556
557 assert!(! parsed.is_empty());
559
560 let offset = parsed.get(&("PIONEER", "BD-RW BDR-X13U"))
562 .expect("Unable to find BDR-X13U offset.");
563 assert_eq!(*offset, 667);
564 }
565}