cdtoc/
lib.rs

1/*!
2# CDTOC
3
4[![docs.rs](https://img.shields.io/docsrs/cdtoc.svg?style=flat-square&label=docs.rs)](https://docs.rs/cdtoc/)
5[![changelog](https://img.shields.io/crates/v/cdtoc.svg?style=flat-square&label=changelog&color=9b59b6)](https://github.com/Blobfolio/cdtoc/blob/master/CHANGELOG.md)<br>
6[![crates.io](https://img.shields.io/crates/v/cdtoc.svg?style=flat-square&label=crates.io)](https://crates.io/crates/cdtoc)
7[![ci](https://img.shields.io/github/actions/workflow/status/Blobfolio/cdtoc/ci.yaml?label=ci&style=flat-square)](https://github.com/Blobfolio/cdtoc/actions)
8[![deps.rs](https://deps.rs/crate/cdtoc/latest/status.svg?style=flat-square&label=deps.rs)](https://deps.rs/crate/cdtoc/)<br>
9[![license](https://img.shields.io/badge/license-wtfpl-ff1493?style=flat-square)](https://en.wikipedia.org/wiki/WTFPL)
10[![contributions welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square&label=contributions)](https://github.com/Blobfolio/cdtoc/issues)
11
12
13
14CDTOC is a simple Rust library for parsing and working with audio CD tables of contents, namely in the form of [CDTOC-style](https://forum.dbpoweramp.com/showthread.php?16705-FLAC-amp-Ogg-Vorbis-Storage-of-CDTOC&s=3ca0c65ee58fc45489103bb1c39bfac0&p=76686&viewfull=1#post76686) metadata values.
15
16By default it can also generate disc IDs for services like [AccurateRip](http://accuraterip.com/), [CDDB](https://en.wikipedia.org/wiki/CDDB), [CUETools Database](http://cue.tools/wiki/CUETools_Database), and [MusicBrainz](https://musicbrainz.org/), but you can disable the corresponding crate feature(s) — `accuraterip`, `cddb`, `ctdb`, and `musicbrainz` respectively — to shrink the dependency tree if you don't need that functionality.
17
18
19
20## Examples
21
22```
23use cdtoc::Toc;
24
25// From a CDTOC string.
26let toc1 = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
27
28// From the raw parts.
29let toc2 = Toc::from_parts(
30    vec![150, 11563, 25174, 45863],
31    None,
32    55370,
33).unwrap();
34
35// Either way gets you to the same place.
36assert_eq!(toc1, toc2);
37
38// You can also get a CDTOC-style string back at any time:
39assert_eq!(toc1.to_string(), "4+96+2D2B+6256+B327+D84A");
40```
41
42
43
44## De/Serialization
45
46The optional `serde` crate feature can be enabled to expose de/serialization implementations for this library's types:
47
48| Type | Format | Notes |
49| ---- | ------ | ----- |
50| [`AccurateRip`] | `String` | |
51| [`Cddb`] | `String` | |
52| [`Duration`] | `u64` | |
53| [`ShaB64`] | `String` | MusicBrainz and CTDB IDs. |
54| [`Toc`] | `String` | |
55| [`Track`] | `Map` | |
56| [`TrackPosition`] | `String` | |
57*/
58
59#![deny(
60	clippy::allow_attributes_without_reason,
61	clippy::correctness,
62	unreachable_pub,
63	unsafe_code,
64)]
65
66#![warn(
67	clippy::complexity,
68	clippy::nursery,
69	clippy::pedantic,
70	clippy::perf,
71	clippy::style,
72
73	clippy::allow_attributes,
74	clippy::clone_on_ref_ptr,
75	clippy::create_dir,
76	clippy::filetype_is_file,
77	clippy::format_push_string,
78	clippy::get_unwrap,
79	clippy::impl_trait_in_params,
80	clippy::lossy_float_literal,
81	clippy::missing_assert_message,
82	clippy::missing_docs_in_private_items,
83	clippy::needless_raw_strings,
84	clippy::panic_in_result_fn,
85	clippy::pub_without_shorthand,
86	clippy::rest_pat_in_fully_bound_structs,
87	clippy::semicolon_inside_block,
88	clippy::str_to_string,
89	clippy::string_to_string,
90	clippy::todo,
91	clippy::undocumented_unsafe_blocks,
92	clippy::unneeded_field_pattern,
93	clippy::unseparated_literal_suffix,
94	clippy::unwrap_in_result,
95
96	macro_use_extern_crate,
97	missing_copy_implementations,
98	missing_docs,
99	non_ascii_idents,
100	trivial_casts,
101	trivial_numeric_casts,
102	unused_crate_dependencies,
103	unused_extern_crates,
104	unused_import_braces,
105)]
106
107#![expect(clippy::doc_markdown, reason = "This gets annoying with names like MusicBrainz.")]
108
109#![cfg_attr(docsrs, feature(doc_cfg))]
110
111
112
113mod error;
114mod hex;
115mod time;
116mod track;
117#[cfg(feature = "accuraterip")] mod accuraterip;
118#[cfg(feature = "cddb")] mod cddb;
119#[cfg(feature = "ctdb")] mod ctdb;
120#[cfg(feature = "musicbrainz")] mod musicbrainz;
121#[cfg(feature = "serde")] mod serde;
122#[cfg(feature = "sha1")] mod shab64;
123
124pub use error::TocError;
125pub use time::Duration;
126pub use track::{
127	Track,
128	Tracks,
129	TrackPosition,
130};
131#[cfg(feature = "accuraterip")] pub use accuraterip::AccurateRip;
132#[cfg(feature = "cddb")] pub use cddb::Cddb;
133#[cfg(feature = "sha1")] pub use shab64::ShaB64;
134
135use dactyl::traits::HexToUnsigned;
136use std::fmt;
137
138
139
140#[cfg(any(feature = "ctdb", feature = "musicbrainz"))]
141/// # Track Zeroes.
142///
143/// The CTDB and Musicbrainz IDs hash one hundred tracks' worth of hexified
144/// sector values, eight bytes each. This serves as the default.
145const TRACK_ZEROES: [[u8; 8]; 100] = [[b'0'; 8]; 100];
146
147
148
149#[derive(Debug, Clone, Eq, Hash, PartialEq)]
150/// # CDTOC.
151///
152/// This struct holds a CD's parsed table of contents.
153///
154/// You can initialize it using a [CDTOC-style](https://forum.dbpoweramp.com/showthread.php?16705-FLAC-amp-Ogg-Vorbis-Storage-of-CDTOC&s=3ca0c65ee58fc45489103bb1c39bfac0&p=76686&viewfull=1#post76686) metadata value
155/// via [`Toc::from_cdtoc`] or manually with [`Toc::from_parts`].
156///
157/// Once parsed, you can obtain things like the [number of audio tracks](Toc::audio_len),
158/// their [sector positions](Toc::audio_sectors), information about the [session(s)](Toc::kind)
159/// and so on.
160///
161/// Many online databases derive their unique disc IDs using tables of content
162/// too. [`Toc`] can give you the following, provided the corresponding crate
163/// feature(s) are enabled:
164///
165/// | Service | Feature | Method |
166/// | ------- | ------- | ------ |
167/// | [AccurateRip](http://accuraterip.com/) | `accuraterip` | [`Toc::accuraterip_id`] |
168/// | [CDDB](https://en.wikipedia.org/wiki/CDDB) | `cddb` | [`Toc::cddb_id`] |
169/// | [CUETools Database](http://cue.tools/wiki/CUETools_Database) | `ctdb` | [`Toc::ctdb_id`] |
170/// | [MusicBrainz](https://musicbrainz.org/) | `musicbrainz` | [`Toc::musicbrainz_id`] |
171///
172/// If you don't care about any of those, import this crate with
173/// `default-features = false` to skip the overhead.
174///
175/// ## Examples
176///
177/// ```
178/// use cdtoc::Toc;
179///
180/// // From a CDTOC string.
181/// let toc1 = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
182///
183/// // From the raw parts.
184/// let toc2 = Toc::from_parts(
185///     vec![150, 11563, 25174, 45863],
186///     None,
187///     55370,
188/// ).unwrap();
189///
190/// // Either way gets you to the same place.
191/// assert_eq!(toc1, toc2);
192///
193/// // You can also get a CDTOC-style string back at any time:
194/// assert_eq!(toc1.to_string(), "4+96+2D2B+6256+B327+D84A");
195/// ```
196pub struct Toc {
197	/// # Disc Type.
198	kind: TocKind,
199
200	/// # Start Sectors for Each Audio Track.
201	audio: Vec<u32>,
202
203	/// # Start Sector for Data Track (if any).
204	data: u32,
205
206	/// # Leadout Sector.
207	leadout: u32,
208}
209
210impl fmt::Display for Toc {
211	#[expect(clippy::cast_possible_truncation, reason = "False positive.")]
212	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213		/// # Trim Leading Zeroes.
214		const fn trim_leading_zeroes(mut src: &[u8]) -> &[u8] {
215			while let [b'0', rest @ ..] = src { src = rest; }
216			src
217		}
218
219		let mut out = Vec::with_capacity(128);
220
221		// Audio track count.
222		let audio_len = self.audio.len() as u8;
223		let buf = hex::upper_encode_u8(audio_len);
224		if 16 <= audio_len { out.push(buf[0]); }
225		out.push(buf[1]);
226
227		/// # Helper: Add Track to Buffer.
228		macro_rules! push {
229			($v:expr) => (
230				out.push(b'+');
231				out.extend_from_slice(trim_leading_zeroes(&hex::upper_encode_u32($v)));
232			);
233		}
234
235		// The sectors.
236		for v in &self.audio { push!(*v); }
237
238		// And finally some combination of data and leadout.
239		match self.kind {
240			TocKind::Audio => { push!(self.leadout); },
241			TocKind::CDExtra => {
242				push!(self.data);
243				push!(self.leadout);
244			},
245			TocKind::DataFirst => {
246				push!(self.leadout);
247
248				// Handle this manually since there's the weird X marker.
249				out.push(b'+');
250				out.push(b'X');
251				out.extend_from_slice(trim_leading_zeroes(&hex::upper_encode_u32(self.data)));
252			},
253		}
254
255		std::str::from_utf8(&out)
256			.map_err(|_| fmt::Error)
257			.and_then(|s| <str as fmt::Display>::fmt(s, f))
258	}
259}
260
261impl Toc {
262	/// # From CDTOC Metadata Tag.
263	///
264	/// Instantiate a new [`Toc`] from a CDTOC metadata tag value, of the
265	/// format described [here](https://forum.dbpoweramp.com/showthread.php?16705-FLAC-amp-Ogg-Vorbis-Storage-of-CDTOC&s=3ca0c65ee58fc45489103bb1c39bfac0&p=76686&viewfull=1#post76686).
266	///
267	/// ## Examples
268	///
269	/// ```
270	/// use cdtoc::Toc;
271	///
272	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
273	/// ```
274	///
275	/// ## Errors
276	///
277	/// This will return an error if the tag value is improperly formatted, the
278	/// audio track count is outside `1..=99`, there are too many or too few
279	/// sectors, the leadin is less than `150`, or the sectors are ordered
280	/// incorrectly.
281	pub fn from_cdtoc<S>(src: S) -> Result<Self, TocError>
282	where S: AsRef<str> {
283		let (audio, data, leadout) = parse_cdtoc_metadata(src.as_ref().as_bytes())?;
284		Self::from_parts(audio, data, leadout)
285	}
286
287	/// # From Durations.
288	///
289	/// This will attempt to create an audio-only [`Toc`] from the track
290	/// durations. (Needless to say, this will only work if all tracks are
291	/// present and in the right order!)
292	///
293	/// If you happen to know the disc's true leadin offset you can specify it,
294	/// otherwise the "industry default" value of `150` will be assumed.
295	///
296	/// To create a mixed-mode [`Toc`] from scratch, use [`Toc::from_parts`]
297	/// instead so you can specify the location of the data session.
298	///
299	/// ## Examples
300	///
301	/// ```
302	/// use cdtoc::{Toc, Duration};
303	///
304	/// let toc = Toc::from_durations(
305	///     [
306	///         Duration::from(46650_u64),
307	///         Duration::from(41702_u64),
308	///         Duration::from(30295_u64),
309	///         Duration::from(37700_u64),
310	///         Duration::from(40050_u64),
311	///         Duration::from(53985_u64),
312	///         Duration::from(37163_u64),
313	///         Duration::from(59902_u64),
314	///     ],
315	///     None,
316	/// ).unwrap();
317	/// assert_eq!(
318	///     toc.to_string(),
319	///     "8+96+B6D0+159B6+1D00D+26351+2FFC3+3D2A4+463CF+54DCD",
320	/// );
321	/// ```
322	///
323	/// ## Errors
324	///
325	/// This will return an error if the track count is outside `1..=99`, the
326	/// leadin is less than 150, or the sectors overflow `u32`.
327	pub fn from_durations<I>(src: I, leadin: Option<u32>) -> Result<Self, TocError>
328	where I: IntoIterator<Item=Duration> {
329		let mut last: u32 = leadin.unwrap_or(150);
330		let mut audio: Vec<u32> = vec![last];
331		for d in src {
332			let next = u32::try_from(d.sectors())
333				.ok()
334				.and_then(|n| last.checked_add(n))
335				.ok_or(TocError::SectorSize)?;
336			audio.push(next);
337			last = next;
338		}
339
340		let leadout = audio.remove(audio.len() - 1);
341		Self::from_parts(audio, None, leadout)
342	}
343
344	/// # From Parts.
345	///
346	/// Instantiate a new [`Toc`] by manually specifying the (starting) sectors
347	/// for each audio track, data track (if any), and the leadout.
348	///
349	/// If a data track is supplied, it must fall between the last audio track
350	/// and leadout, or come before either.
351	///
352	/// ## Examples
353	///
354	/// ```
355	/// use cdtoc::Toc;
356	///
357	/// let toc = Toc::from_parts(
358	///     vec![150, 11563, 25174, 45863],
359	///     None,
360	///     55370,
361	/// ).unwrap();
362	///
363	/// assert_eq!(toc.to_string(), "4+96+2D2B+6256+B327+D84A");
364	///
365	/// // Sanity matters; the leadin, for example, can't be less than 150.
366	/// assert!(Toc::from_parts(
367	///     vec![0, 10525],
368	///     None,
369	///     15000,
370	/// ).is_err());
371	/// ```
372	///
373	/// ## Errors
374	///
375	/// This will return an error if the audio track count is outside `1..=99`,
376	/// the leadin is less than `150`, or the sectors are in the wrong order.
377	pub fn from_parts(audio: Vec<u32>, data: Option<u32>, leadout: u32)
378	-> Result<Self, TocError> {
379		// Check length.
380		let audio_len = audio.len();
381		if 0 == audio_len { return Err(TocError::NoAudio); }
382		if 99 < audio_len { return Err(TocError::TrackCount); }
383
384		// Audio leadin must be at least 150.
385		if audio[0] < 150 { return Err(TocError::LeadinSize); }
386
387		// Audio is out of order?
388		if
389			(1 < audio_len && audio.windows(2).any(|pair| pair[1] <= pair[0])) ||
390			leadout <= audio[audio_len - 1]
391		{
392			return Err(TocError::SectorOrder);
393		}
394
395		// Figure out the kind and validate the data sector.
396		let kind =
397			if let Some(d) = data {
398				if d < audio[0] { TocKind::DataFirst }
399				else if audio[audio_len - 1] < d && d < leadout {
400					TocKind::CDExtra
401				}
402				else { return Err(TocError::SectorOrder); }
403			}
404			else { TocKind::Audio };
405
406		Ok(Self { kind, audio, data: data.unwrap_or_default(), leadout })
407	}
408
409	/// # Set Audio Leadin.
410	///
411	/// Set the audio leadin, nudging all entries up or down accordingly (
412	/// including data and leadout).
413	///
414	/// Note: this method cannot be used for data-first mixed-mode CDs.
415	///
416	/// ## Examples
417	///
418	/// ```
419	/// use cdtoc::{Toc, TocKind};
420	///
421	/// let mut toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
422	/// assert_eq!(toc.audio_leadin(), 150);
423	///
424	/// // Bump it up to 182.
425	/// assert!(toc.set_audio_leadin(182).is_ok());
426	/// assert_eq!(toc.audio_leadin(), 182);
427	/// assert_eq!(
428	///     toc.to_string(),
429	///     "4+B6+2D4B+6276+B347+D86A",
430	/// );
431	///
432	/// // Back down to 150.
433	/// assert!(toc.set_audio_leadin(150).is_ok());
434	/// assert_eq!(toc.audio_leadin(), 150);
435	/// assert_eq!(
436	///     toc.to_string(),
437	///     "4+96+2D2B+6256+B327+D84A",
438	/// );
439	///
440	/// // For CD-Extra, the data track will get nudged too.
441	/// toc = Toc::from_cdtoc("3+96+2D2B+6256+B327+D84A").unwrap();
442	/// assert_eq!(toc.kind(), TocKind::CDExtra);
443	/// assert_eq!(toc.audio_leadin(), 150);
444	/// assert_eq!(toc.data_sector(), Some(45863));
445	///
446	/// assert!(toc.set_audio_leadin(182).is_ok());
447	/// assert_eq!(toc.audio_leadin(), 182);
448	/// assert_eq!(toc.data_sector(), Some(45895));
449	///
450	/// // And back again.
451	/// assert!(toc.set_audio_leadin(150).is_ok());
452	/// assert_eq!(toc.audio_leadin(), 150);
453	/// assert_eq!(toc.data_sector(), Some(45863));
454	/// ```
455	///
456	/// ## Errors
457	///
458	/// This will return an error if the leadin is less than `150`, the CD
459	/// format is data-first, or the nudging causes the sectors to overflow
460	/// `u32`.
461	pub fn set_audio_leadin(&mut self, leadin: u32) -> Result<(), TocError> {
462		use std::cmp::Ordering;
463
464		if leadin < 150 { Err(TocError::LeadinSize) }
465		else if matches!(self.kind, TocKind::DataFirst) {
466			Err(TocError::Format(TocKind::DataFirst))
467		}
468		else {
469			let current = self.audio_leadin();
470			match leadin.cmp(&current) {
471				// Nudge downward.
472				Ordering::Less => {
473					let diff = current - leadin;
474					for v in &mut self.audio { *v -= diff; }
475					if self.has_data() { self.data -= diff; }
476					self.leadout -= diff;
477				},
478				// Nudge upward.
479				Ordering::Greater => {
480					let diff = leadin - current;
481					for v in &mut self.audio {
482						*v = v.checked_add(diff).ok_or(TocError::SectorSize)?;
483					}
484					if self.has_data() {
485						self.data = self.data.checked_add(diff)
486							.ok_or(TocError::SectorSize)?;
487					}
488					self.leadout = self.leadout.checked_add(diff)
489						.ok_or(TocError::SectorSize)?;
490				},
491				// Noop.
492				Ordering::Equal => {},
493			}
494
495			Ok(())
496		}
497	}
498
499	/// # Set Media Kind.
500	///
501	/// This method can be used to override the table of content's derived
502	/// media format.
503	///
504	/// This is weird, but might come in handy if you need to correct a not-
505	/// quite-right CDTOC metadata tag value, such as one that accidentally
506	/// included the data session in its leading track count or ordered the
507	/// sectors of a data-audio CD sequentially.
508	///
509	/// ## Examples
510	///
511	/// ```
512	/// use cdtoc::{Toc, TocKind};
513	///
514	/// // This will be interpreted as audio-only.
515	/// let mut toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
516	///
517	/// // If the track count was wrong and it is really a mixed-mode CD-Extra
518	/// // disc, this will fix it right up:
519	/// assert!(toc.set_kind(TocKind::CDExtra).is_ok());
520	/// assert_eq!(
521	///     toc.to_string(),
522	///     "3+96+2D2B+6256+B327+D84A",
523	/// );
524	/// ```
525	///
526	/// ## Errors
527	///
528	/// This will return an error if there aren't enough sectors or tracks for
529	/// the new kind.
530	pub fn set_kind(&mut self, kind: TocKind) -> Result<(), TocError> {
531		match (self.kind, kind) {
532			// The last "audio" track is really data.
533			(TocKind::Audio, TocKind::CDExtra) => {
534				let len = self.audio.len();
535				if len == 1 { return Err(TocError::NoAudio); }
536				self.data = self.audio.remove(len - 1);
537			},
538			// The first "audio" track is really data.
539			(TocKind::Audio, TocKind::DataFirst) => {
540				if self.audio.len() == 1 { return Err(TocError::NoAudio); }
541				self.data = self.audio.remove(0);
542			},
543			// The "data" track is the really the last audio track.
544			(TocKind::CDExtra, TocKind::Audio) => {
545				self.audio.push(self.data);
546				self.data = 0;
547			},
548			// The "data" track is the really the last audio track.
549			(TocKind::DataFirst, TocKind::Audio) => {
550				self.audio.insert(0, self.data);
551				self.data = 0;
552			},
553			// Data should come first, not last.
554			(TocKind::CDExtra, TocKind::DataFirst) => {
555				// Move the old track to the end of the audio list and replace
556				// with the first.
557				self.audio.push(self.data);
558				self.data = self.audio.remove(0);
559			},
560			// Data should come last, not first.
561			(TocKind::DataFirst, TocKind::CDExtra) => {
562				// Move the old track to the front of the audio list and
563				// replace with the last.
564				self.audio.insert(0, self.data);
565				self.data = self.audio.remove(self.audio.len() - 1);
566			},
567			// Noop.
568			_ => return Ok(()),
569		}
570
571		self.kind = kind;
572		Ok(())
573	}
574}
575
576impl Toc {
577	#[must_use]
578	/// # Audio Leadin.
579	///
580	/// Return the leadin of the audio session, sometimes called the "offset".
581	/// In practice, this is just where the first audio track begins.
582	///
583	/// ## Examples
584	///
585	/// ```
586	/// use cdtoc::Toc;
587	///
588	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
589	/// assert_eq!(toc.audio_leadin(), 150);
590	/// ```
591	pub const fn audio_leadin(&self) -> u32 {
592		if let [ out, .. ] = self.audio.as_slice() { *out }
593		// This isn't actually reachable.
594		else { 150 }
595	}
596
597	#[must_use]
598	/// # Normalized Audio Leadin.
599	///
600	/// This is the same as [`Toc::audio_leadin`], but _without_ the mandatory
601	/// 150-sector CD lead-in.
602	///
603	/// ## Examples
604	///
605	/// ```
606	/// use cdtoc::Toc;
607	///
608	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
609	/// assert_eq!(toc.audio_leadin(), 150);
610	/// assert_eq!(toc.audio_leadin_normalized(), 0);
611	/// ```
612	pub const fn audio_leadin_normalized(&self) -> u32 { self.audio_leadin() - 150 }
613
614	#[must_use]
615	/// # Audio Leadout.
616	///
617	/// Return the leadout for the audio session. This is usually the same as
618	/// [`Toc::leadout`], but for CD-Extra discs, the audio leadout is actually
619	/// the start of the data, minus a gap of `11_400`.
620	///
621	/// ## Examples
622	///
623	/// ```
624	/// use cdtoc::Toc;
625	///
626	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
627	/// assert_eq!(toc.audio_leadout(), 55370);
628	/// ```
629	pub const fn audio_leadout(&self) -> u32 {
630		if matches!(self.kind, TocKind::CDExtra) {
631			self.data.saturating_sub(11_400)
632		}
633		else { self.leadout }
634	}
635
636	#[must_use]
637	/// # Normalized Audio Leadout.
638	///
639	/// This is the same as [`Toc::audio_leadout`], but _without_ the mandatory
640	/// 150-sector CD lead-in.
641	///
642	/// ## Examples
643	///
644	/// ```
645	/// use cdtoc::Toc;
646	///
647	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
648	/// assert_eq!(toc.audio_leadout(), 55370);
649	/// assert_eq!(toc.audio_leadout_normalized(), 55220);
650	/// ```
651	pub const fn audio_leadout_normalized(&self) -> u32 {
652		self.audio_leadout() - 150
653	}
654
655	#[must_use]
656	/// # Number of Audio Tracks.
657	///
658	/// ## Examples
659	///
660	/// ```
661	/// use cdtoc::Toc;
662	///
663	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
664	/// assert_eq!(toc.audio_len(), 4);
665	/// ```
666	pub const fn audio_len(&self) -> usize { self.audio.len() }
667
668	#[must_use]
669	/// # Audio Sectors.
670	///
671	/// Return the starting positions of each audio track.
672	///
673	/// ## Examples
674	///
675	/// ```
676	/// use cdtoc::Toc;
677	///
678	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
679	/// assert_eq!(toc.audio_sectors(), &[150, 11563, 25174, 45863]);
680	/// ```
681	pub const fn audio_sectors(&self) -> &[u32] { self.audio.as_slice() }
682
683	#[expect(clippy::cast_possible_truncation, reason = "False positive.")]
684	#[must_use]
685	/// # Audio Track.
686	///
687	/// Return the details of a given audio track on the disc, or `None` if the
688	/// track number is out of range.
689	pub fn audio_track(&self, num: usize) -> Option<Track> {
690		let len = self.audio_len();
691		if num == 0 || len < num { None }
692		else {
693			let from = self.audio[num - 1];
694			let to =
695				if num < len { self.audio[num] }
696				else { self.audio_leadout() };
697
698			Some(Track {
699				num: num as u8,
700				pos: TrackPosition::from((num, len)),
701				from,
702				to,
703			})
704		}
705	}
706
707	#[must_use]
708	/// # Audio Tracks.
709	///
710	/// Return an iterator of [`Track`] details covering the whole album.
711	pub const fn audio_tracks(&self) -> Tracks<'_> {
712		Tracks::new(self.audio.as_slice(), self.audio_leadout())
713	}
714
715	#[must_use]
716	/// # Data Sector.
717	///
718	/// Return the starting position of the data track, if any.
719	///
720	/// ## Examples
721	///
722	/// ```
723	/// use cdtoc::Toc;
724	///
725	/// // No data here.
726	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
727	/// assert_eq!(toc.data_sector(), None);
728	///
729	/// // This CD-Extra has data, though!
730	/// let toc = Toc::from_cdtoc("3+96+2D2B+6256+B327+D84A").unwrap();
731	/// assert_eq!(toc.data_sector(), Some(45_863));
732	/// ```
733	pub const fn data_sector(&self) -> Option<u32> {
734		if self.kind.has_data() { Some(self.data) }
735		else { None }
736	}
737
738	#[must_use]
739	/// # Normalized Data Sector.
740	///
741	/// This is the same as [`Toc::data_sector`], but _without_ the mandatory
742	/// 150-sector CD lead-in.
743	///
744	/// ## Examples
745	///
746	/// ```
747	/// use cdtoc::Toc;
748	///
749	/// // No data here.
750	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
751	/// assert_eq!(toc.data_sector(), None);
752	///
753	/// // This CD-Extra has data, though!
754	/// let toc = Toc::from_cdtoc("3+96+2D2B+6256+B327+D84A").unwrap();
755	/// assert_eq!(toc.data_sector(), Some(45_863));
756	/// assert_eq!(toc.data_sector_normalized(), Some(45_713));
757	/// ```
758	pub const fn data_sector_normalized(&self) -> Option<u32> {
759		if self.kind.has_data() { Some(self.data.saturating_sub(150)) }
760		else { None }
761	}
762
763	#[must_use]
764	/// # Has Data?
765	///
766	/// This returns `true` for mixed-mode CDs and `false` for audio-only ones.
767	///
768	/// ## Examples
769	///
770	/// ```
771	/// use cdtoc::Toc;
772	///
773	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
774	/// assert_eq!(toc.has_data(), false);
775	///
776	/// let toc = Toc::from_cdtoc("3+96+2D2B+6256+B327+D84A").unwrap();
777	/// assert_eq!(toc.has_data(), true);
778	/// ```
779	pub const fn has_data(&self) -> bool { self.kind.has_data() }
780
781	#[must_use]
782	/// # HTOA Pre-gap "Track".
783	///
784	/// Return a `Track` object representing the space between the mandatory
785	/// disc leadin (`150`) and the start of the first audio track, if any.
786	///
787	/// Such regions usually only contain a small amount of silence — extra
788	/// padding, basically — but every once in a while might be a secret bonus
789	/// song.
790	///
791	/// ## Examples
792	///
793	/// ```
794	/// use cdtoc::Toc;
795	///
796	/// // This disc has no HTOA.
797	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
798	/// assert!(toc.htoa().is_none());
799	///
800	/// // But this one does!
801	/// let toc = Toc::from_cdtoc("15+247E+2BEC+4AF4+7368+9704+B794+E271+110D0+12B7A+145C1+16CAF+195CF+1B40F+1F04A+21380+2362D+2589D+2793D+2A760+2DA32+300E1+32B46").unwrap();
802	/// let htoa = toc.htoa().unwrap();
803	/// assert!(htoa.is_htoa()); // Should always be true.
804	///
805	/// // HTOAs have no track number.
806	/// assert_eq!(htoa.number(), 0);
807	///
808	/// // Their position is also technically invalid.
809	/// assert!(! htoa.position().is_valid());
810	///
811	/// // Their ranges are normal, though.
812	/// assert_eq!(htoa.sector_range(), 150..9342);
813	/// ```
814	pub const fn htoa(&self) -> Option<Track> {
815		let leadin = self.audio_leadin();
816		if leadin == 150 || matches!(self.kind, TocKind::DataFirst) { None }
817		else {
818			Some(Track {
819				num: 0,
820				pos: TrackPosition::Invalid,
821				from: 150,
822				to: leadin,
823			})
824		}
825	}
826
827	#[must_use]
828	/// # CD Format.
829	///
830	/// This returns the [`TocKind`] corresponding to the table of contents,
831	/// useful if you want to know whether or not the disc has a data session,
832	/// and where it is in relation to the audio session.
833	///
834	/// ## Examples
835	///
836	/// ```
837	/// use cdtoc::{Toc, TocKind};
838	///
839	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
840	/// assert_eq!(toc.kind(), TocKind::Audio);
841	///
842	/// let toc = Toc::from_cdtoc("3+96+2D2B+6256+B327+D84A").unwrap();
843	/// assert_eq!(toc.kind(), TocKind::CDExtra);
844	///
845	/// let toc = Toc::from_cdtoc("3+2D2B+6256+B327+D84A+X96").unwrap();
846	/// assert_eq!(toc.kind(), TocKind::DataFirst);
847	/// ```
848	pub const fn kind(&self) -> TocKind { self.kind }
849
850	#[must_use]
851	/// # Absolute Leadin.
852	///
853	/// Return the offset of the first track (no matter the session type).
854	///
855	/// ## Examples
856	///
857	/// ```
858	/// use cdtoc::Toc;
859	///
860	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
861	/// assert_eq!(toc.leadin(), 150);
862	/// ```
863	pub const fn leadin(&self) -> u32 {
864		if matches!(self.kind, TocKind::DataFirst) { self.data }
865		else { self.audio_leadin() }
866	}
867
868	#[must_use]
869	/// # Normalized Absolute Leadin.
870	///
871	/// This is the same as [`Toc::leadin`], but _without_ the mandatory
872	/// 150-sector CD lead-in.
873	///
874	/// ## Examples
875	///
876	/// ```
877	/// use cdtoc::Toc;
878	///
879	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
880	/// assert_eq!(toc.leadin(), 150);
881	/// assert_eq!(toc.leadin_normalized(), 0);
882	/// ```
883	pub const fn leadin_normalized(&self) -> u32 {
884		self.leadin().saturating_sub(150)
885	}
886
887	#[must_use]
888	/// # Absolute Leadout.
889	///
890	/// Return the disc leadout, regardless of whether it marks the end of the
891	/// audio or data session.
892	///
893	/// ## Examples
894	///
895	/// ```
896	/// use cdtoc::Toc;
897	///
898	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
899	/// assert_eq!(toc.leadout(), 55_370);
900	/// ```
901	pub const fn leadout(&self) -> u32 { self.leadout }
902
903	#[must_use]
904	/// # Normalized Absolute Leadout.
905	///
906	/// This is the same as [`Toc::leadout`], but _without_ the mandatory
907	/// 150-sector CD lead-in.
908	///
909	/// ## Examples
910	///
911	/// ```
912	/// use cdtoc::Toc;
913	///
914	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
915	/// assert_eq!(toc.leadout(), 55_370);
916	/// assert_eq!(toc.leadout_normalized(), 55_220);
917	/// ```
918	pub const fn leadout_normalized(&self) -> u32 { self.leadout - 150 }
919
920	#[must_use]
921	/// # Duration.
922	///
923	/// Return the total duration of all audio tracks.
924	///
925	/// ## Examples
926	///
927	/// ```
928	/// use cdtoc::{Duration, Toc};
929	///
930	/// let toc = Toc::from_cdtoc("4+96+2D2B+6256+B327+D84A").unwrap();
931	/// assert_eq!(
932	///     toc.duration(),
933	///     toc.audio_tracks().map(|t| t.duration()).sum(),
934	/// );
935	/// ```
936	pub const fn duration(&self) -> Duration {
937		Duration((self.audio_leadout() - self.audio_leadin()) as u64)
938	}
939}
940
941
942
943#[derive(Debug, Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
944/// # CD Format.
945///
946/// This enum is used to differentiate between audio-only and mixed-mode discs
947/// because that ultimately determines the formatting of CDTOC metadata values
948/// and various derived third-party IDs.
949pub enum TocKind {
950	#[default]
951	/// # Audio-Only.
952	Audio,
953
954	/// # Mixed w/ Trailing Data Session.
955	CDExtra,
956
957	/// # Mixed w/ Leading Data Session.
958	///
959	/// This would only be possible with a weird homebrew CD-R; retail CDs
960	/// place their data sessions at the end.
961	DataFirst,
962}
963
964impl fmt::Display for TocKind {
965	#[inline]
966	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
967		<str as fmt::Display>::fmt(self.as_str(), f)
968	}
969}
970
971impl TocKind {
972	#[must_use]
973	/// # As Str.
974	///
975	/// Return the value as a string slice.
976	pub const fn as_str(self) -> &'static str {
977		match self {
978			Self::Audio => "audio-only",
979			Self::CDExtra => "CD-Extra",
980			Self::DataFirst => "data+audio",
981		}
982	}
983
984	#[must_use]
985	/// # Has Data?
986	///
987	/// Returns `true` if the format is mixed-mode.
988	///
989	/// ## Examples
990	///
991	/// ```
992	/// use cdtoc::TocKind;
993	///
994	/// // Yep!
995	/// assert!(TocKind::CDExtra.has_data());
996	/// assert!(TocKind::DataFirst.has_data());
997	///
998	/// // Nope!
999	/// assert!(! TocKind::Audio.has_data());
1000	/// ```
1001	pub const fn has_data(self) -> bool {
1002		matches!(self, Self::CDExtra | Self::DataFirst)
1003	}
1004}
1005
1006
1007
1008/// # Parse CDTOC Metadata.
1009///
1010/// This parses the audio track count and sector positions from a CDTOC-style
1011/// metadata tag value. It will return a parsing error if the formatting is
1012/// grossly wrong, but will not validate the sanity of the count/parts.
1013fn parse_cdtoc_metadata(src: &[u8]) -> Result<(Vec<u32>, Option<u32>, u32), TocError> {
1014	let src = src.trim_ascii();
1015	let mut split = src.split(|b| b'+'.eq(b));
1016
1017	// The number of audio tracks comes first.
1018	let audio_len = split.next()
1019		.and_then(u8::htou)
1020		.ok_or(TocError::TrackCount)?;
1021
1022	// We should have starting positions for just as many tracks.
1023	let sectors: Vec<u32> = split
1024		.by_ref()
1025		.take(usize::from(audio_len))
1026		.map(u32::htou)
1027		.collect::<Option<Vec<u32>>>()
1028		.ok_or(TocError::SectorSize)?;
1029
1030	// Make sure we actually do.
1031	let sectors_len = sectors.len();
1032	if 0 == sectors_len { return Err(TocError::NoAudio); }
1033	if sectors_len != usize::from(audio_len) {
1034		return Err(TocError::SectorCount(audio_len, sectors_len));
1035	}
1036
1037	// There should be at least one more entry to mark the audio leadout.
1038	let last1 = split.next()
1039		.ok_or(TocError::SectorCount(audio_len, sectors_len - 1))?;
1040	let last1 = u32::htou(last1).ok_or(TocError::SectorSize)?;
1041
1042	// If there is yet another entry, we've got a mixed-mode disc.
1043	if let Some(last2) = split.next() {
1044		// Unlike the other values, this entry might have an x-prefix to denote
1045		// a non-standard data-first position.
1046		let last2 = u32::htou(last2)
1047			.or_else(||
1048				last2.strip_prefix(b"X").or_else(|| last2.strip_prefix(b"x"))
1049					.and_then(u32::htou)
1050			)
1051			.ok_or(TocError::SectorSize)?;
1052
1053		// That should be that!
1054		let remaining = split.count();
1055		if remaining == 0 {
1056			// "last1" is data, "last2" is leadout.
1057			if last1 < last2 {
1058				Ok((sectors, Some(last1), last2))
1059			}
1060			// "last2" is data, "last1" is leadout.
1061			else {
1062				Ok((sectors, Some(last2), last1))
1063			}
1064		}
1065		// Too many sectors!
1066		else {
1067			Err(TocError::SectorCount(audio_len, sectors_len + remaining))
1068		}
1069	}
1070	// A typical audio-only CD.
1071	else { Ok((sectors, None, last1)) }
1072}
1073
1074
1075
1076#[cfg(test)]
1077mod tests {
1078	use super::*;
1079	use brunch as _;
1080	use serde_json as _;
1081
1082	const CDTOC_AUDIO: &str = "B+96+5DEF+A0F2+F809+1529F+1ACB3+20CBC+24E14+2AF17+2F4EA+35BDD+3B96D";
1083	const CDTOC_EXTRA: &str = "A+96+3757+696D+C64F+10A13+14DA2+19E88+1DBAA+213A4+2784E+2D7AF+36F11";
1084	const CDTOC_DATA_AUDIO: &str = "A+3757+696D+C64F+10A13+14DA2+19E88+1DBAA+213A4+2784E+2D7AF+36F11+X96";
1085
1086	#[test]
1087	/// # Test Audio-Only Parsing.
1088	fn t_audio() {
1089		let toc = Toc::from_cdtoc(CDTOC_AUDIO).expect("Unable to parse CDTOC_AUDIO.");
1090		let sectors = vec![
1091			150,
1092			24047,
1093			41202,
1094			63497,
1095			86687,
1096			109_747,
1097			134_332,
1098			151_060,
1099			175_895,
1100			193_770,
1101			220_125,
1102		];
1103		assert_eq!(toc.audio_len(), 11);
1104		assert_eq!(toc.audio_sectors(), &sectors);
1105		assert_eq!(toc.data_sector(), None);
1106		assert!(!toc.has_data());
1107		assert_eq!(toc.kind(), TocKind::Audio);
1108		assert_eq!(toc.audio_leadin(), 150);
1109		assert_eq!(toc.audio_leadout(), 244_077);
1110		assert_eq!(toc.leadin(), 150);
1111		assert_eq!(toc.leadout(), 244_077);
1112		assert_eq!(toc.to_string(), CDTOC_AUDIO);
1113
1114		// This should match when built with the equivalent parts.
1115		assert_eq!(
1116			Toc::from_parts(sectors, None, 244_077),
1117			Ok(toc),
1118		);
1119
1120		// Let's also quickly test that a long TOC works gets the audio track
1121		// count right.
1122		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")
1123			.expect("Long TOC failed.");
1124		assert_eq!(toc.audio_len(), 32);
1125		assert_eq!(
1126			toc.to_string(),
1127			"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"
1128		);
1129
1130		// And one more with a hexish track count.
1131		let toc = Toc::from_cdtoc("10+96+2B4E+4C51+6B3C+9E08+CD43+FC99+13A55+164B8+191C9+1C0FF+1F613+21B5A+23F70+27A4A+2C20D+2FC65").unwrap();
1132		assert_eq!(toc.audio_len(), 16);
1133		assert_eq!(
1134			toc.to_string(),
1135			"10+96+2B4E+4C51+6B3C+9E08+CD43+FC99+13A55+164B8+191C9+1C0FF+1F613+21B5A+23F70+27A4A+2C20D+2FC65"
1136		);
1137	}
1138
1139	#[test]
1140	/// # Test CD-Extra Parsing.
1141	fn t_extra() {
1142		let toc = Toc::from_cdtoc(CDTOC_EXTRA).expect("Unable to parse CDTOC_EXTRA.");
1143		let sectors = vec![
1144			150,
1145			14167,
1146			26989,
1147			50767,
1148			68115,
1149			85410,
1150			106_120,
1151			121_770,
1152			136_100,
1153			161_870,
1154		];
1155		assert_eq!(toc.audio_len(), 10);
1156		assert_eq!(toc.audio_sectors(), &sectors);
1157		assert_eq!(toc.data_sector(), Some(186_287));
1158		assert!(toc.has_data());
1159		assert_eq!(toc.kind(), TocKind::CDExtra);
1160		assert_eq!(toc.audio_leadin(), 150);
1161		assert_eq!(toc.audio_leadout(), 174_887);
1162		assert_eq!(toc.leadin(), 150);
1163		assert_eq!(toc.leadout(), 225_041);
1164		assert_eq!(toc.to_string(), CDTOC_EXTRA);
1165
1166		// This should match when built with the equivalent parts.
1167		assert_eq!(
1168			Toc::from_parts(sectors, Some(186_287), 225_041),
1169			Ok(toc),
1170		);
1171	}
1172
1173	#[test]
1174	/// # Test Data-First Parsing.
1175	fn t_data_first() {
1176		let toc = Toc::from_cdtoc(CDTOC_DATA_AUDIO)
1177			.expect("Unable to parse CDTOC_DATA_AUDIO.");
1178		let sectors = vec![
1179			14167,
1180			26989,
1181			50767,
1182			68115,
1183			85410,
1184			106_120,
1185			121_770,
1186			136_100,
1187			161_870,
1188			186_287,
1189		];
1190		assert_eq!(toc.audio_len(), 10);
1191		assert_eq!(toc.audio_sectors(), &sectors);
1192		assert_eq!(toc.data_sector(), Some(150));
1193		assert!(toc.has_data());
1194		assert_eq!(toc.kind(), TocKind::DataFirst);
1195		assert_eq!(toc.audio_leadin(), 14167);
1196		assert_eq!(toc.audio_leadout(), 225_041);
1197		assert_eq!(toc.leadin(), 150);
1198		assert_eq!(toc.leadout(), 225_041);
1199		assert_eq!(toc.to_string(), CDTOC_DATA_AUDIO);
1200
1201		// This should match when built with the equivalent parts.
1202		assert_eq!(
1203			Toc::from_parts(sectors, Some(150), 225_041),
1204			Ok(toc),
1205		);
1206	}
1207
1208	#[test]
1209	/// # Test Metadata Failures.
1210	fn t_bad() {
1211		for i in [
1212			"A+96+3757+696D+C64F+10A13+14DA2+19E88+1DBAA+213A4+2784E+2D7AF+36F11+36F12",
1213			"A+96+3757+696D+C64F+10A13+14DA2+19E88+1DBAA+213A4+2784E",
1214			"0+96",
1215			"A+96+3757+696D+C64F+10A13+14DA2+19E88+2784E+1DBAA+213A4+2D7AF+36F11",
1216		] {
1217			assert!(Toc::from_cdtoc(i).is_err());
1218		}
1219	}
1220
1221	#[test]
1222	#[expect(clippy::cognitive_complexity, reason = "It is what it is.")]
1223	/// # Test Kind Conversions.
1224	fn t_rekind() {
1225		// Start with audio.
1226		let mut toc = Toc::from_cdtoc(CDTOC_AUDIO)
1227			.expect("Unable to parse CDTOC_AUDIO.");
1228
1229		// To CD-Extra.
1230		assert!(toc.set_kind(TocKind::CDExtra).is_ok());
1231		assert_eq!(toc.audio_len(), 10);
1232		assert_eq!(
1233			toc.audio_sectors(),
1234			&[
1235				150,
1236				24047,
1237				41202,
1238				63497,
1239				86687,
1240				109_747,
1241				134_332,
1242				151_060,
1243				175_895,
1244				193_770,
1245			]
1246		);
1247		assert_eq!(toc.data_sector(), Some(220_125));
1248		assert!(toc.has_data());
1249		assert_eq!(toc.kind(), TocKind::CDExtra);
1250		assert_eq!(toc.audio_leadin(), 150);
1251		assert_eq!(toc.audio_leadout(), 208_725);
1252		assert_eq!(toc.leadin(), 150);
1253		assert_eq!(toc.leadout(), 244_077);
1254
1255		// Back again.
1256		assert!(toc.set_kind(TocKind::Audio).is_ok());
1257		assert_eq!(Toc::from_cdtoc(CDTOC_AUDIO).unwrap(), toc);
1258
1259		// To data-audio.
1260		assert!(toc.set_kind(TocKind::DataFirst).is_ok());
1261		assert_eq!(toc.audio_len(), 10);
1262		assert_eq!(
1263			toc.audio_sectors(),
1264			&[
1265				24047,
1266				41202,
1267				63497,
1268				86687,
1269				109_747,
1270				134_332,
1271				151_060,
1272				175_895,
1273				193_770,
1274				220_125,
1275			]
1276		);
1277		assert_eq!(toc.data_sector(), Some(150));
1278		assert!(toc.has_data());
1279		assert_eq!(toc.kind(), TocKind::DataFirst);
1280		assert_eq!(toc.audio_leadin(), 24047);
1281		assert_eq!(toc.audio_leadout(), 244_077);
1282		assert_eq!(toc.leadin(), 150);
1283		assert_eq!(toc.leadout(), 244_077);
1284
1285		// Back again.
1286		assert!(toc.set_kind(TocKind::Audio).is_ok());
1287		assert_eq!(Toc::from_cdtoc(CDTOC_AUDIO).unwrap(), toc);
1288
1289		// Now test data-to-other-data conversions.
1290		toc = Toc::from_cdtoc(CDTOC_EXTRA)
1291			.expect("Unable to parse CDTOC_EXTRA.");
1292		let extra = toc.clone();
1293		let data_audio = Toc::from_cdtoc(CDTOC_DATA_AUDIO)
1294			.expect("Unable to parse CDTOC_DATA_AUDIO.");
1295
1296		// To data-audio.
1297		assert!(toc.set_kind(TocKind::DataFirst).is_ok());
1298		assert_eq!(toc, data_audio);
1299
1300		// And back again.
1301		assert!(toc.set_kind(TocKind::CDExtra).is_ok());
1302		assert_eq!(toc, extra);
1303	}
1304}