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