flac_codec/metadata/
cuesheet.rs

1// Copyright 2025 Brian Langenberger
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use crate::Error;
10use crate::metadata::CuesheetError;
11use crate::metadata::contiguous::{Adjacent, Contiguous};
12use bitstream_io::{BitRead, BitWrite, FromBitStream, ToBitStream};
13use std::num::NonZero;
14use std::str::FromStr;
15
16/// An ASCII digit, for the catalog number
17#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
18pub enum Digit {
19    /// U+0030
20    Digit0 = 48,
21    /// U+0031
22    Digit1 = 49,
23    /// U+0032
24    Digit2 = 50,
25    /// U+0033
26    Digit3 = 51,
27    /// U+0034
28    Digit4 = 52,
29    /// U+0035
30    Digit5 = 53,
31    /// U+0036
32    Digit6 = 54,
33    /// U+0037
34    Digit7 = 55,
35    /// U+0038
36    Digit8 = 56,
37    /// U+0039
38    Digit9 = 57,
39}
40
41impl TryFrom<u8> for Digit {
42    type Error = u8;
43
44    fn try_from(u: u8) -> Result<Digit, u8> {
45        match u {
46            48 => Ok(Self::Digit0),
47            49 => Ok(Self::Digit1),
48            50 => Ok(Self::Digit2),
49            51 => Ok(Self::Digit3),
50            52 => Ok(Self::Digit4),
51            53 => Ok(Self::Digit5),
52            54 => Ok(Self::Digit6),
53            55 => Ok(Self::Digit7),
54            56 => Ok(Self::Digit8),
55            57 => Ok(Self::Digit9),
56            u => Err(u),
57        }
58    }
59}
60
61impl TryFrom<char> for Digit {
62    type Error = CuesheetError;
63
64    fn try_from(c: char) -> Result<Digit, CuesheetError> {
65        match c {
66            '0' => Ok(Self::Digit0),
67            '1' => Ok(Self::Digit1),
68            '2' => Ok(Self::Digit2),
69            '3' => Ok(Self::Digit3),
70            '4' => Ok(Self::Digit4),
71            '5' => Ok(Self::Digit5),
72            '6' => Ok(Self::Digit6),
73            '7' => Ok(Self::Digit7),
74            '8' => Ok(Self::Digit8),
75            '9' => Ok(Self::Digit9),
76            _ => Err(CuesheetError::InvalidCatalogNumber),
77        }
78    }
79}
80
81impl From<Digit> for u8 {
82    fn from(d: Digit) -> u8 {
83        d as u8
84    }
85}
86
87impl std::fmt::Display for Digit {
88    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
89        match self {
90            Self::Digit0 => '0'.fmt(f),
91            Self::Digit1 => '1'.fmt(f),
92            Self::Digit2 => '2'.fmt(f),
93            Self::Digit3 => '3'.fmt(f),
94            Self::Digit4 => '4'.fmt(f),
95            Self::Digit5 => '5'.fmt(f),
96            Self::Digit6 => '6'.fmt(f),
97            Self::Digit7 => '7'.fmt(f),
98            Self::Digit8 => '8'.fmt(f),
99            Self::Digit9 => '9'.fmt(f),
100        }
101    }
102}
103
104/// An offset for CD-DA
105///
106/// These must be evenly divisible by 588 samples
107#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
108pub struct CDDAOffset {
109    offset: u64,
110}
111
112impl CDDAOffset {
113    const SAMPLES_PER_SECTOR: u64 = 44100 / 75;
114}
115
116impl std::ops::Sub for CDDAOffset {
117    type Output = Self;
118
119    fn sub(self, rhs: Self) -> Self {
120        Self {
121            offset: self.offset - rhs.offset,
122        }
123    }
124}
125
126impl FromStr for CDDAOffset {
127    type Err = ();
128
129    fn from_str(s: &str) -> Result<Self, ()> {
130        let (mm, rest) = s.split_once(':').ok_or(())?;
131        let (ss, ff) = rest.split_once(':').ok_or(())?;
132
133        let ff: u64 = ff.parse().ok().filter(|ff| *ff < 75).ok_or(())?;
134        let ss: u64 = ss.parse().ok().filter(|ss| *ss < 60).ok_or(())?;
135        let mm: u64 = mm.parse().map_err(|_| ())?;
136
137        Ok(Self {
138            offset: (ff + ss * 75 + mm * 75 * 60) * 588,
139        })
140    }
141}
142
143impl std::fmt::Display for CDDAOffset {
144    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
145        self.offset.fmt(f)
146    }
147}
148
149impl From<CDDAOffset> for u64 {
150    fn from(o: CDDAOffset) -> Self {
151        o.offset
152    }
153}
154
155impl TryFrom<u64> for CDDAOffset {
156    type Error = u64;
157
158    fn try_from(offset: u64) -> Result<Self, Self::Error> {
159        offset
160            .is_multiple_of(Self::SAMPLES_PER_SECTOR)
161            .then_some(Self { offset })
162            .ok_or(offset)
163    }
164}
165
166impl std::ops::Add for CDDAOffset {
167    type Output = Self;
168
169    fn add(self, rhs: CDDAOffset) -> Self {
170        // if both are already divisible by 588,
171        // their added quantities will also
172        // be divsible by 588
173        Self {
174            offset: self.offset + rhs.offset,
175        }
176    }
177}
178
179impl FromBitStream for CDDAOffset {
180    type Error = Error;
181
182    fn from_reader<R: BitRead + ?Sized>(r: &mut R) -> Result<Self, Self::Error> {
183        Ok(Self {
184            offset: r.read_to().map_err(Error::Io).and_then(|o| {
185                ((o % Self::SAMPLES_PER_SECTOR) == 0)
186                    .then_some(o)
187                    .ok_or(CuesheetError::InvalidCDDAOffset.into())
188            })?,
189        })
190    }
191}
192
193impl ToBitStream for CDDAOffset {
194    type Error = std::io::Error;
195
196    fn to_writer<W: BitWrite + ?Sized>(&self, w: &mut W) -> Result<(), Self::Error> {
197        // value already checked for divisibility,
198        // so no need to check it again
199        w.write_from(self.offset)
200    }
201}
202
203impl Adjacent for CDDAOffset {
204    fn valid_first(&self) -> bool {
205        self.offset == 0
206    }
207
208    fn is_next(&self, previous: &Self) -> bool {
209        self.offset > previous.offset
210    }
211}
212
213/// The track number for lead-out tracks
214#[derive(Debug, Copy, Clone, Eq, PartialEq)]
215pub struct LeadOut;
216
217impl LeadOut {
218    /// Lead-out track number for CD-DA discs
219    pub const CDDA: NonZero<u8> = NonZero::new(170).unwrap();
220
221    /// Lead-out track number for non-CD-DA discs
222    pub const NON_CDDA: NonZero<u8> = NonZero::new(255).unwrap();
223}
224
225/// An International Standard Recording Code value
226///
227/// These are used to assign a unique identifier
228/// to sound and music video recordings.
229///
230/// This is a 12 character code which may be
231/// delimited by optional dashes.
232///
233/// ```text
234///  letters     digits
235///       ↓↓     ↓↓
236///       AA-6Q7-20-00047
237///          ↑↑↑    ↑↑↑↑↑
238/// alphanumeric    digits
239/// ```
240///
241/// The first five characters are the prefix code.
242/// The following two digits are the year of reference.
243/// The final five digits are the designation code.
244#[derive(Debug, Clone, Eq, PartialEq)]
245pub struct ISRCString(String);
246
247impl std::fmt::Display for ISRCString {
248    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
249        self.0.fmt(f)
250    }
251}
252
253impl AsRef<str> for ISRCString {
254    fn as_ref(&self) -> &str {
255        self.0.as_str()
256    }
257}
258
259impl FromStr for ISRCString {
260    type Err = CuesheetError;
261
262    fn from_str(s: &str) -> Result<Self, Self::Err> {
263        use std::borrow::Cow;
264
265        fn filter_split(s: &str, amt: usize, f: impl Fn(char) -> bool) -> Option<&str> {
266            s.split_at_checked(amt)
267                .and_then(|(prefix, rest)| prefix.chars().all(f).then_some(rest))
268        }
269
270        // strip out dashes if necessary
271        let isrc: Cow<'_, str> = if s.contains('-') {
272            s.chars().filter(|c| *c != '-').collect::<String>().into()
273        } else {
274            s.into()
275        };
276
277        filter_split(&isrc, 2, |c| c.is_ascii_alphabetic())
278            .and_then(|s| filter_split(s, 3, |c| c.is_ascii_alphanumeric()))
279            .and_then(|s| filter_split(s, 2, |c| c.is_ascii_digit()))
280            .and_then(|s| s.chars().all(|c| c.is_ascii_digit()).then_some(()))
281            .map(|()| ISRCString(isrc.into_owned()))
282            .ok_or(CuesheetError::InvalidISRC)
283    }
284}
285
286/// An optional ISRC value
287#[derive(Default, Debug, Clone, Eq, PartialEq)]
288pub enum ISRC {
289    /// An undefined ISRC value in which all bits are 0
290    #[default]
291    None,
292    /// A defined ISRC value matching the ISRC format
293    String(ISRCString),
294}
295
296impl std::fmt::Display for ISRC {
297    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
298        match self {
299            Self::String(s) => s.fmt(f),
300            Self::None => "".fmt(f),
301        }
302    }
303}
304
305impl FromBitStream for ISRC {
306    type Error = Error;
307
308    fn from_reader<R: BitRead + ?Sized>(r: &mut R) -> Result<Self, Error> {
309        let isrc = r.read_to::<[u8; 12]>()?;
310        if isrc.iter().all(|b| *b == 0) {
311            Ok(ISRC::None)
312        } else {
313            let s = str::from_utf8(&isrc).map_err(|_| CuesheetError::InvalidISRC)?;
314
315            Ok(ISRC::String(s.parse()?))
316        }
317    }
318}
319
320impl ToBitStream for ISRC {
321    type Error = std::io::Error;
322
323    fn to_writer<W: BitWrite + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
324        w.write_from(match self {
325            Self::String(isrc) => {
326                let mut o = [0; 12];
327                o.iter_mut()
328                    .zip(isrc.as_ref().as_bytes())
329                    .for_each(|(o, i)| *o = *i);
330                o
331            }
332            Self::None => [0; 12],
333        })
334    }
335}
336
337impl AsRef<str> for ISRC {
338    fn as_ref(&self) -> &str {
339        match self {
340            Self::String(s) => s.as_ref(),
341            Self::None => "",
342        }
343    }
344}
345
346impl FromStr for ISRC {
347    type Err = CuesheetError;
348
349    fn from_str(s: &str) -> Result<Self, Self::Err> {
350        ISRCString::from_str(s).map(ISRC::String)
351    }
352}
353
354/// An individual CUESHEET track
355///
356/// | Bits | Field | Meaning |
357/// |-----:|------:|---------|
358/// | 64   | `offset` | offset of first index point, in samples
359/// | 8    | `number` | track number
360/// | 12×8 | `isrc`   | track ISRC
361/// | 1    | `non_audio`| whether track is non-audio
362/// | 1    | `pre_emphasis` | whether track has pre-emphasis
363/// | 6+13×8 | padding | all 0 bits
364/// | 8    | point count | number index points
365/// |      | | index point₀, index point₁, …
366///
367#[derive(Debug, Clone, Eq, PartialEq)]
368pub struct Track<O, N, P> {
369    /// Offset of first index point
370    ///
371    /// In samples relative to the beginning of the FLAC audio stream.
372    ///
373    /// For CD-DA, the track offset must always be divisible by 588.
374    /// This is because for audio CDs, tracks must always begin
375    /// on CD frame boundaries.  Since each CD frame
376    /// is 1/75th of a second, and CDs have 44,100 samples per second,
377    /// 44100 ÷ 75 = 588.
378    ///
379    /// Non-CD-DA discs have no such restriction.
380    pub offset: O,
381
382    /// Track number
383    ///
384    /// | Disc Type  | Range                  | Lead-Out Track
385    /// |-----------:|:----------------------:|---------------
386    /// | CD-DA      | 1 ≤ track number ≤ 99  | 170
387    /// | Non-CD-DA  | 1 ≤ track number < 255 | 255
388    pub number: N,
389
390    /// Track's ISRC
391    pub isrc: ISRC,
392
393    /// Whether track is non-audio
394    pub non_audio: bool,
395
396    /// Whether track has pre-emphasis
397    pub pre_emphasis: bool,
398
399    /// Track's index points
400    ///
401    /// | Disc Type | Lead-Out Track | Index Points          |
402    /// |----------:|:--------------:|-----------------------|
403    /// | CD-DA     | No             | not more than 100     |
404    /// | CD-DA     | Yes            | 0                     |
405    /// | Non-CD-DA | No             | not more than 255     |
406    /// | Non-CD-DA | Yes            | 0                     |
407    pub index_points: P,
408}
409
410impl<const MAX: usize, O: Adjacent, N: Adjacent> Adjacent for Track<O, N, IndexVec<MAX, O>> {
411    fn valid_first(&self) -> bool {
412        self.offset.valid_first() && self.number.valid_first()
413    }
414
415    fn is_next(&self, previous: &Self) -> bool {
416        self.number.is_next(&previous.number) && self.offset.is_next(previous.index_points.last())
417    }
418}
419
420/// A Generic track suitable for display
421///
422/// The lead-out track has a track number of `None`.
423pub type TrackGeneric = Track<u64, Option<u8>, Vec<Index<u64>>>;
424
425/// A CD-DA CUESHEET track
426pub type TrackCDDA = Track<CDDAOffset, NonZero<u8>, IndexVec<100, CDDAOffset>>;
427
428impl FromBitStream for TrackCDDA {
429    type Error = Error;
430
431    fn from_reader<R: BitRead + ?Sized>(r: &mut R) -> Result<Self, Self::Error> {
432        let offset = r.parse()?;
433        let number = r
434            .read_to()
435            .map_err(Error::Io)
436            .and_then(|s| NonZero::new(s).ok_or(Error::from(CuesheetError::InvalidIndexPoint)))?;
437        let isrc = r.parse()?;
438        let non_audio = r.read_bit()?;
439        let pre_emphasis = r.read_bit()?;
440        r.skip(6 + 13 * 8)?;
441        let index_point_count = r.read_to::<u8>()?;
442
443        Ok(Self {
444            offset,
445            number,
446            isrc,
447            non_audio,
448            pre_emphasis,
449            // IndexVec guarantees at least 1 index point
450            // Contiguous guarantees there's no more than MAX index points
451            // and that they're all in order
452            index_points: IndexVec::try_from(
453                Contiguous::try_collect((0..index_point_count).map(|_| r.parse()))
454                    .map_err(|_| Error::from(CuesheetError::IndexPointsOutOfSequence))??,
455            )?,
456        })
457    }
458}
459
460impl ToBitStream for TrackCDDA {
461    type Error = Error;
462
463    fn to_writer<W: BitWrite + ?Sized>(&self, w: &mut W) -> Result<(), Self::Error> {
464        w.build(&self.offset)?;
465        w.write_from(self.number.get())?;
466        w.build(&self.isrc)?;
467        w.write_bit(self.non_audio)?;
468        w.write_bit(self.pre_emphasis)?;
469        w.pad(6 + 13 * 8)?;
470        w.write_from::<u8>(self.index_points.len().try_into().unwrap())?;
471        for point in self.index_points.iter() {
472            w.build(point)?;
473        }
474        Ok(())
475    }
476}
477
478/// A non-CD-DA CUESHEET track
479pub type TrackNonCDDA = Track<u64, NonZero<u8>, IndexVec<256, u64>>;
480
481impl FromBitStream for TrackNonCDDA {
482    type Error = Error;
483
484    fn from_reader<R: BitRead + ?Sized>(r: &mut R) -> Result<Self, Self::Error> {
485        let offset = r.read_to()?;
486        let number = r
487            .read_to()
488            .map_err(Error::Io)
489            .and_then(|s| NonZero::new(s).ok_or(Error::from(CuesheetError::InvalidIndexPoint)))?;
490        let isrc = r.parse()?;
491        let non_audio = r.read_bit()?;
492        let pre_emphasis = r.read_bit()?;
493        r.skip(6 + 13 * 8)?;
494        let index_point_count = r.read_to::<u8>()?;
495
496        Ok(Self {
497            offset,
498            number,
499            isrc,
500            non_audio,
501            pre_emphasis,
502            // IndexVec guarantees at least 1 index point
503            // Contiguous guarantees there's no more than MAX index points
504            // and that they're all in order
505            index_points: IndexVec::try_from(
506                Contiguous::try_collect((0..index_point_count).map(|_| r.parse()))
507                    .map_err(|_| Error::from(CuesheetError::IndexPointsOutOfSequence))??,
508            )?,
509        })
510    }
511}
512
513impl ToBitStream for TrackNonCDDA {
514    type Error = Error;
515
516    fn to_writer<W: BitWrite + ?Sized>(&self, w: &mut W) -> Result<(), Self::Error> {
517        w.write_from(self.offset)?;
518        w.write_from(self.number.get())?;
519        w.build(&self.isrc)?;
520        w.write_bit(self.non_audio)?;
521        w.write_bit(self.pre_emphasis)?;
522        w.pad(6 + 13 * 8)?;
523        w.write_from::<u8>(self.index_points.len().try_into().unwrap())?;
524        for point in self.index_points.iter() {
525            w.build(point)?;
526        }
527        Ok(())
528    }
529}
530
531/// A CD-DA CUESHEET lead-out track
532pub type LeadOutCDDA = Track<CDDAOffset, LeadOut, ()>;
533
534impl FromBitStream for LeadOutCDDA {
535    type Error = Error;
536
537    fn from_reader<R: BitRead + ?Sized>(r: &mut R) -> Result<Self, Self::Error> {
538        let offset = r.parse()?;
539        let number = r.read_to::<u8>().map_err(Error::Io).and_then(|n| {
540            NonZero::new(n)
541                .filter(|n| *n == LeadOut::CDDA)
542                .map(|_| LeadOut)
543                .ok_or(CuesheetError::TracksOutOfSequence.into())
544        })?;
545        let isrc = r.parse()?;
546        let non_audio = r.read_bit()?;
547        let pre_emphasis = r.read_bit()?;
548        r.skip(6 + 13 * 8)?;
549        match r.read_to::<u8>()? {
550            0 => Ok(Self {
551                offset,
552                number,
553                isrc,
554                non_audio,
555                pre_emphasis,
556                index_points: (),
557            }),
558            // because parsing a cuesheet generates a lead-out
559            // automatically, this error can only only occur when
560            // reading from metadata blocks
561            _ => Err(CuesheetError::IndexPointsInLeadout.into()),
562        }
563    }
564}
565
566impl ToBitStream for LeadOutCDDA {
567    type Error = Error;
568
569    fn to_writer<W: BitWrite + ?Sized>(&self, w: &mut W) -> Result<(), Self::Error> {
570        w.build(&self.offset)?;
571        w.write_from(LeadOut::CDDA.get())?;
572        w.build(&self.isrc)?;
573        w.write_bit(self.non_audio)?;
574        w.write_bit(self.pre_emphasis)?;
575        w.pad(6 + 13 * 8)?;
576        w.write_from::<u8>(0)?;
577        Ok(())
578    }
579}
580
581impl LeadOutCDDA {
582    /// Creates new lead-out track with the given offset
583    ///
584    /// Lead-out offset must be contiguous with existing tracks
585    pub fn new(last: Option<&TrackCDDA>, offset: CDDAOffset) -> Result<Self, CuesheetError> {
586        match last {
587            Some(track) if *track.index_points.last() >= offset => Err(CuesheetError::ShortLeadOut),
588            _ => Ok(LeadOutCDDA {
589                offset,
590                number: LeadOut,
591                isrc: ISRC::None,
592                non_audio: false,
593                pre_emphasis: false,
594                index_points: (),
595            }),
596        }
597    }
598}
599
600/// A non-CD-DA CUESHEET lead-out track
601pub type LeadOutNonCDDA = Track<u64, LeadOut, ()>;
602
603impl FromBitStream for LeadOutNonCDDA {
604    type Error = Error;
605
606    fn from_reader<R: BitRead + ?Sized>(r: &mut R) -> Result<Self, Self::Error> {
607        let offset = r.read_to()?;
608        let number = r.read_to::<u8>().map_err(Error::Io).and_then(|n| {
609            NonZero::new(n)
610                .filter(|n| *n == LeadOut::NON_CDDA)
611                .map(|_| LeadOut)
612                .ok_or(CuesheetError::TracksOutOfSequence.into())
613        })?;
614        let isrc = r.parse()?;
615        let non_audio = r.read_bit()?;
616        let pre_emphasis = r.read_bit()?;
617        r.skip(6 + 13 * 8)?;
618        match r.read_to::<u8>()? {
619            0 => Ok(Self {
620                offset,
621                number,
622                isrc,
623                non_audio,
624                pre_emphasis,
625                index_points: (),
626            }),
627            // because parsing a cuesheet generates a lead-out
628            // automatically, this error can only only occur when
629            // reading from metadata blocks
630            _ => Err(CuesheetError::IndexPointsInLeadout.into()),
631        }
632    }
633}
634
635impl ToBitStream for LeadOutNonCDDA {
636    type Error = Error;
637
638    fn to_writer<W: BitWrite + ?Sized>(&self, w: &mut W) -> Result<(), Self::Error> {
639        w.write_from(self.offset)?;
640        w.write_from::<u8>(LeadOut::NON_CDDA.get())?;
641        w.build(&self.isrc)?;
642        w.write_bit(self.non_audio)?;
643        w.write_bit(self.pre_emphasis)?;
644        w.pad(6 + 13 * 8)?;
645        w.write_from::<u8>(0)?;
646        Ok(())
647    }
648}
649
650impl LeadOutNonCDDA {
651    /// Creates new lead-out track with the given offset
652    pub fn new(last: Option<&TrackNonCDDA>, offset: u64) -> Result<Self, CuesheetError> {
653        match last {
654            Some(track) if *track.index_points.last() >= offset => Err(CuesheetError::ShortLeadOut),
655            _ => Ok(LeadOutNonCDDA {
656                offset,
657                number: LeadOut,
658                isrc: ISRC::None,
659                non_audio: false,
660                pre_emphasis: false,
661                index_points: (),
662            }),
663        }
664    }
665}
666
667/// An individual CUESHEET track index point
668///
669/// | Bits | Field | Meaning |
670/// |-----:|------:|---------|
671/// | 64   | `offset` | index point offset, in samples
672/// | 8    | `number` | index point number
673/// | 3×8  | padding  | all 0 bits
674///
675#[derive(Copy, Clone, Eq, PartialEq, Debug)]
676pub struct Index<O> {
677    /// Offset in samples from beginning of track
678    pub offset: O,
679
680    /// Track index point number
681    pub number: u8,
682}
683
684impl<O: Adjacent> Adjacent for Index<O> {
685    fn valid_first(&self) -> bool {
686        self.offset.valid_first() && matches!(self.number, 0 | 1)
687    }
688
689    fn is_next(&self, previous: &Self) -> bool {
690        self.offset.is_next(&previous.offset) && self.number == previous.number + 1
691    }
692}
693
694impl FromBitStream for Index<CDDAOffset> {
695    type Error = Error;
696
697    fn from_reader<R: BitRead + ?Sized>(r: &mut R) -> Result<Self, Self::Error> {
698        let offset = r.parse()?;
699        let number = r.read_to()?;
700        r.skip(3 * 8)?;
701        Ok(Self { offset, number })
702    }
703}
704
705impl FromBitStream for Index<u64> {
706    type Error = Error;
707
708    fn from_reader<R: BitRead + ?Sized>(r: &mut R) -> Result<Self, Self::Error> {
709        let offset = r.read_to()?;
710        let number = r.read_to()?;
711        r.skip(3 * 8)?;
712        Ok(Self { offset, number })
713    }
714}
715
716impl ToBitStream for Index<CDDAOffset> {
717    type Error = std::io::Error;
718
719    fn to_writer<W: BitWrite + ?Sized>(&self, w: &mut W) -> Result<(), Self::Error> {
720        w.build(&self.offset)?;
721        w.write_from(self.number)?;
722        w.pad(3 * 8)
723    }
724}
725
726impl ToBitStream for Index<u64> {
727    type Error = std::io::Error;
728
729    fn to_writer<W: BitWrite + ?Sized>(&self, w: &mut W) -> Result<(), Self::Error> {
730        w.write_from(self.offset)?;
731        w.write_from(self.number)?;
732        w.pad(3 * 8)
733    }
734}
735
736/// A Vec of Indexes with the given offset type
737///
738/// Tracks other than the lead-out are required
739/// to have at least one `INDEX 01` index point,
740/// which specifies the beginning of the track.
741/// An `INDEX 00` pre-gap point is optional.
742///
743/// `MAX` is the maximum number of index points
744/// this can hold, including the first.
745/// This is 100 for CD-DA (`00` to `99`, inclusive)
746/// and 254 for non-CD-DA cuesheets.
747#[derive(Clone, Debug, Eq, PartialEq)]
748pub struct IndexVec<const MAX: usize, O: Adjacent> {
749    // pre-gap
750    index_00: Option<Index<O>>,
751    // start of track
752    index_01: Index<O>,
753    // remaining index points
754    remainder: Box<[Index<O>]>,
755}
756
757impl<const MAX: usize, O: Adjacent> IndexVec<MAX, O> {
758    /// Returns number of `Index` points in `IndexVec`
759    // This method never returns 0, so cannot be empty,
760    // so it doesn't make sense to implement is_empty()
761    // for it because it would always return false.
762    #[allow(clippy::len_without_is_empty)]
763    pub fn len(&self) -> usize {
764        // because we're created from a Contiguous Vec
765        // whose size must be <= usize,
766        // our len is 1 less than usize, so len() + 1
767        // can never overflow
768        usize::from(self.index_00.is_some()) + 1 + self.remainder.len()
769    }
770
771    /// Iterates over shared references of all `Index` points
772    pub fn iter(&self) -> impl Iterator<Item = &Index<O>> {
773        self.index_00
774            .iter()
775            .chain(std::iter::once(&self.index_01))
776            .chain(&self.remainder)
777    }
778
779    /// Returns offset of track pre-gap, any
780    ///
781    /// This corresponds to `INDEX 00`
782    pub fn pre_gap(&self) -> Option<&O> {
783        match &self.index_00 {
784            Some(Index { offset, .. }) => Some(offset),
785            None => None,
786        }
787    }
788
789    /// Returns offset of track start
790    ///
791    /// This corresponds to `INDEX 01`
792    pub fn start(&self) -> &O {
793        &self.index_01.offset
794    }
795
796    /// Returns shared reference to final item
797    ///
798    /// Since `IndexVec` must always contain at least
799    /// one item, this method is infallible
800    pub fn last(&self) -> &O {
801        match self.remainder.last() {
802            Some(Index { offset, .. }) => offset,
803            None => self.start(),
804        }
805    }
806}
807
808impl<const MAX: usize, O: Adjacent> TryFrom<Contiguous<MAX, Index<O>>> for IndexVec<MAX, O> {
809    type Error = CuesheetError;
810
811    fn try_from(items: Contiguous<MAX, Index<O>>) -> Result<Self, CuesheetError> {
812        use std::collections::VecDeque;
813
814        let mut items: VecDeque<Index<O>> = items.into();
815
816        match items.pop_front().ok_or(CuesheetError::NoIndexPoints)? {
817            index_00 @ Index { number: 0, .. } => Ok(Self {
818                index_00: Some(index_00),
819                index_01: items
820                    .pop_front()
821                    .filter(|i| i.number == 1)
822                    .ok_or(CuesheetError::IndexPointsOutOfSequence)?,
823                remainder: Vec::from(items).into_boxed_slice(),
824            }),
825            index_01 @ Index { number: 1, .. } => Ok(Self {
826                index_00: None,
827                index_01,
828                remainder: Vec::from(items).into_boxed_slice(),
829            }),
830            Index { .. } => Err(CuesheetError::IndexPointsOutOfSequence),
831        }
832    }
833}