chdlady-disc 0.2.2

Disc geometries and physical media formats for chdlady
//! CDRDAO TOC file parser and formatter.
use crate::error::DiscError;
use crate::types::{CdToc, CdTrack, CdTrackType, DiscType, Msf};

/// Parser and generator for CDRDAO-compatible `.toc` disc descriptors.
#[derive(Debug, Default)]
pub struct CdrdaoToc;

impl CdrdaoToc {
    /// Formats a `CdToc` into CDRDAO `.toc` descriptor text.
    pub fn format(toc: &CdToc, bin_filename: &str) -> String {
        let mut out = String::new();

        let any_xa = toc.tracks.iter().any(|t| {
            matches!(
                t.track_type,
                CdTrackType::Mode2Form1 | CdTrackType::Mode2Form2 | CdTrackType::Mode2FormMix
            )
        });
        let all_audio = toc.tracks.iter().all(|t| t.track_type.is_audio());

        if any_xa {
            out.push_str("CD_ROM_XA\n\n\n");
        } else if all_audio {
            out.push_str("CD_DA\n\n\n");
        } else {
            out.push_str("CD_ROM\n\n\n");
        }

        let mut cumulative_bytes: u64 = 0;
        for (i, track) in toc.tracks.iter().enumerate() {
            if i > 0 {
                out.push('\n');
            }
            out.push_str(&format!("// Track {}\n", track.track_num));
            out.push_str(&format!(
                "TRACK {}\n",
                track.track_type.chd_metadata_type_str()
            ));
            out.push_str("NO COPY\n");

            if track.track_type.is_audio() {
                out.push_str("NO PRE_EMPHASIS\n");
                out.push_str("TWO_CHANNEL_AUDIO\n");
            }

            if track.pregap_frames > 0 {
                let pg_msf = Msf::from_lba(track.pregap_frames);
                let pg_type_str = if track.track_type.is_audio() {
                    "AUDIO"
                } else {
                    track.track_type.chd_metadata_type_str()
                };
                out.push_str(&format!("ZERO {} {}\n", pg_type_str, pg_msf));
            }

            let frames_msf = Msf::from_lba(track.frames);
            let track_bytes = track.frames as u64 * track.track_type.sector_size() as u64;

            if cumulative_bytes > 0 {
                out.push_str(&format!(
                    "DATAFILE \"{}\" #{} {} // length in bytes: {}\n",
                    bin_filename, cumulative_bytes, frames_msf, track_bytes
                ));
            } else {
                out.push_str(&format!(
                    "DATAFILE \"{}\" {} // length in bytes: {}\n",
                    bin_filename, frames_msf, track_bytes
                ));
            }

            if track.pregap_frames > 0 {
                let start_msf = Msf::from_lba(track.pregap_frames);
                out.push_str(&format!("START {}\n", start_msf));
            }

            cumulative_bytes += track_bytes;
            out.push('\n');
        }

        out
    }

    /// Parses a CDRDAO `.toc` descriptor string.
    pub fn parse(content: &str) -> Result<CdToc, DiscError> {
        let mut tracks = Vec::new();
        let mut current_track_num: u32 = 0;
        let mut current_track_type: Option<CdTrackType> = None;
        let mut current_pregap: u32 = 0;
        let mut current_frames: Option<u32> = None;
        let mut current_file: Option<String> = None;
        let mut current_offset: u64 = 0;

        let flush_track = |tracks: &mut Vec<CdTrack>,
                           num: u32,
                           t_type: Option<CdTrackType>,
                           pregap: u32,
                           frames: Option<u32>,
                           file: Option<String>,
                           offset: u64|
         -> Result<(), DiscError> {
            if let (Some(t), Some(f)) = (t_type, frames) {
                let mut track = CdTrack::new(num, t, f);
                track.pregap_frames = pregap;
                track.file_name = file;
                track.file_offset = offset;
                tracks.push(track);
            }
            Ok(())
        };

        for raw_line in content.lines() {
            let line = raw_line.trim();
            if line.is_empty() || line.starts_with("//") {
                continue;
            }

            let tokens: Vec<&str> = line.split_whitespace().collect();
            if tokens.is_empty() {
                continue;
            }

            match tokens[0] {
                "TRACK" => {
                    flush_track(
                        &mut tracks,
                        current_track_num,
                        current_track_type,
                        current_pregap,
                        current_frames,
                        current_file.take(),
                        current_offset,
                    )?;
                    current_track_num += 1;
                    if tokens.len() < 2 {
                        return Err(DiscError::InvalidToc("missing track type".to_string()));
                    }
                    current_track_type = Some(CdTrackType::parse_cue_or_metadata(tokens[1])?);
                    current_pregap = 0;
                    current_frames = None;
                    current_offset = 0;
                }
                "ZERO" => {
                    if tokens.len() >= 3 {
                        let msf = Msf::parse(tokens[2])?;
                        current_pregap += msf.to_lba();
                    }
                }
                "DATAFILE" | "FILE" | "AUDIOFILE" if tokens.len() >= 2 => {
                    let mut file_token = tokens[1];
                    if file_token.starts_with('"') && file_token.ends_with('"') {
                        file_token = &file_token[1..file_token.len() - 1];
                    }
                    crate::types::validate_descriptor_path(file_token)?;
                    current_file = Some(file_token.to_string());

                    let mut msf_idx = 2;
                    if tokens.len() > 2 && tokens[2].starts_with('#') {
                        let off_str = &tokens[2][1..];
                        current_offset = off_str.parse::<u64>().unwrap_or(0);
                        msf_idx = 3;
                    }
                    if tokens.len() > msf_idx {
                        if let Ok(msf) = Msf::parse(tokens[msf_idx]) {
                            current_frames = Some(msf.to_lba());
                        }
                    }
                }
                _ => {}
            }
        }

        flush_track(
            &mut tracks,
            current_track_num,
            current_track_type,
            current_pregap,
            current_frames,
            current_file,
            current_offset,
        )?;

        if tracks.is_empty() {
            return Err(DiscError::InvalidToc("no tracks in TOC".to_string()));
        }

        Ok(CdToc {
            disc_type: DiscType::Cd,
            tracks,
        })
    }
}