use crate::cue::Msf;
use crate::sector::SectorMode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TocMode {
Mode1,
Mode1Raw,
Mode2,
Mode2Raw,
Mode2Form1,
Mode2Form2,
Audio,
Other(String),
}
impl TocMode {
#[must_use]
pub fn sector_mode(&self) -> Option<SectorMode> {
match self {
Self::Mode1 | Self::Mode2Form1 => Some(SectorMode::Iso2048),
Self::Mode1Raw => Some(SectorMode::Raw2352),
Self::Mode2 => Some(SectorMode::Mode2_2336),
Self::Mode2Raw => Some(SectorMode::Raw2352Mode2),
Self::Mode2Form2 | Self::Audio | Self::Other(_) => None,
}
}
#[must_use]
pub fn is_data(&self) -> bool {
self.sector_mode().is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TocTrack {
pub number: u8,
pub mode: TocMode,
pub datafile: Option<String>,
pub file_offset: u64,
pub length_sectors: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TocSheet {
pub disc_type: Option<String>,
pub tracks: Vec<TocTrack>,
}
impl TocSheet {
#[must_use]
pub fn data_track(&self) -> Option<&TocTrack> {
self.tracks.iter().find(|t| t.mode.is_data())
}
}
#[must_use]
pub fn parse(text: &str) -> TocSheet {
let mut sheet = TocSheet::default();
for raw in text.lines() {
let line = strip_comment(raw).trim();
if line.is_empty() {
continue;
}
let mut tok = line.split_whitespace();
match tok.next() {
Some(t @ ("CD_ROM" | "CD_DA" | "CD_ROM_XA")) if sheet.disc_type.is_none() => {
sheet.disc_type = Some(t.to_string());
}
Some("TRACK") => {
let mode = tok.next().map_or(TocMode::Other(String::new()), parse_mode);
let number = u8::try_from(sheet.tracks.len() + 1).unwrap_or(u8::MAX);
sheet.tracks.push(TocTrack {
number,
mode,
datafile: None,
file_offset: 0,
length_sectors: 0,
});
}
Some("DATAFILE" | "AUDIOFILE" | "FILE") => {
if let Some(track) = sheet.tracks.last_mut() {
apply_file_line(track, line);
}
}
_ => {} }
}
sheet
}
fn strip_comment(line: &str) -> &str {
match line.find("//") {
Some(i) => &line[..i],
None => line,
}
}
fn apply_file_line(track: &mut TocTrack, line: &str) {
if let Some(rest) = line.split_once('"').map(|(_, r)| r) {
if let Some((name, after)) = rest.split_once('"') {
track.datafile = Some(name.to_string());
for word in after.split_whitespace() {
if let Some(off) = word.strip_prefix('#') {
if let Ok(v) = off.parse::<u64>() {
track.file_offset = v;
}
} else if let Some(msf) = parse_msf(word) {
track.length_sectors = msf.to_lba();
}
}
}
}
}
fn parse_mode(token: &str) -> TocMode {
match token.to_ascii_uppercase().as_str() {
"MODE1" => TocMode::Mode1,
"MODE1_RAW" => TocMode::Mode1Raw,
"MODE2" => TocMode::Mode2,
"MODE2_RAW" => TocMode::Mode2Raw,
"MODE2_FORM1" => TocMode::Mode2Form1,
"MODE2_FORM2" => TocMode::Mode2Form2,
"AUDIO" => TocMode::Audio,
other => TocMode::Other(other.to_string()),
}
}
fn parse_msf(token: &str) -> Option<Msf> {
let mut parts = token.split(':');
let m = parts.next()?.parse().ok()?;
let s = parts.next()?.parse().ok()?;
let f = parts.next()?.parse().ok()?;
if parts.next().is_some() {
return None;
}
Some(Msf { minutes: m, seconds: s, frames: f })
}