chdlady-disc 0.2.2

Disc geometries and physical media formats for chdlady
//! Shared utilities and codec constants for media creation.

use crate::ccd::CcdSheet;
use crate::cue::CueSheet;
use crate::error::DiscError;
use crate::gdi::GdiSheet;
use crate::mds::MdsDisc;
use crate::nrg::NrgDisc;
use crate::toc::CdrdaoToc;
use crate::types::CdToc;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;

/// Default codecs for raw binary media (`lzma`, `zlib`, `huff`, `flac`).
pub const DEFAULT_RAW_CODECS: [u32; 4] = [
    u32::from_be_bytes(*b"lzma"),
    u32::from_be_bytes(*b"zlib"),
    u32::from_be_bytes(*b"huff"),
    u32::from_be_bytes(*b"flac"),
];

/// Default codecs for hard disk media (`lzma`, `zlib`, `huff`, `flac`).
pub const DEFAULT_HD_CODECS: [u32; 4] = [
    u32::from_be_bytes(*b"lzma"),
    u32::from_be_bytes(*b"zlib"),
    u32::from_be_bytes(*b"huff"),
    u32::from_be_bytes(*b"flac"),
];

/// Default codecs for CD optical media (`cdlz`, `cdzl`, `cdfl`).
pub const DEFAULT_CD_CODECS: [u32; 4] = [
    u32::from_be_bytes(*b"cdlz"),
    u32::from_be_bytes(*b"cdzl"),
    u32::from_be_bytes(*b"cdfl"),
    0,
];

/// Parses a comma-separated string of up to 4 compression codecs into FourCC codes.
pub fn parse_compression_codecs(s: &str) -> Result<[u32; 4], DiscError> {
    let clean = s.trim();
    if clean == "none" {
        return Ok([0, 0, 0, 0]);
    }
    let mut result = [0u32; 4];
    let mut idx = 0;
    for part in clean.split(',') {
        let trimmed = part.trim();
        if trimmed.is_empty() {
            continue;
        }
        if idx >= 4 {
            return Err(DiscError::InvalidMetadata(
                "at most 4 codecs allowed".into(),
            ));
        }
        if trimmed.len() != 4 {
            return Err(DiscError::InvalidMetadata(format!(
                "invalid compressor '{}': must be 4 characters or 'none'",
                trimmed
            )));
        }
        let bytes = trimmed.as_bytes();
        let tag = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
        result[idx] = tag;
        idx += 1;
    }
    Ok(result)
}

/// Locates the `data` chunk in a standard PCM WAV file and returns `(data_offset, data_length)`.
pub fn find_wav_data_chunk(path: &Path) -> Result<(u64, u64), DiscError> {
    let mut file = File::open(path)?;
    let mut riff_hdr = [0u8; 12];
    file.read_exact(&mut riff_hdr)?;
    if &riff_hdr[0..4] != b"RIFF" || &riff_hdr[8..12] != b"WAVE" {
        return Err(DiscError::InvalidCue(format!(
            "file '{}' is not a valid RIFF/WAVE file",
            path.display()
        )));
    }
    let file_size = file.metadata()?.len();
    let mut cur_pos = 12u64;

    while cur_pos + 8 <= file_size {
        file.seek(SeekFrom::Start(cur_pos))?;
        let mut chunk_hdr = [0u8; 8];
        file.read_exact(&mut chunk_hdr)?;
        let chunk_tag = &chunk_hdr[0..4];
        let chunk_len =
            u32::from_le_bytes([chunk_hdr[4], chunk_hdr[5], chunk_hdr[6], chunk_hdr[7]]) as u64;
        cur_pos += 8;

        if chunk_tag == b"data" {
            return Ok((cur_pos, chunk_len));
        }

        cur_pos += chunk_len;
        if !cur_pos.is_multiple_of(2) {
            cur_pos += 1;
        }
    }

    Err(DiscError::InvalidCue(format!(
        "could not find 'data' chunk in WAV file '{}'",
        path.display()
    )))
}

/// Parses an optical disc descriptor file (`.cue`, `.toc`, `.gdi`, `.ccd`, or `.mds`) into a `CdToc`.
pub fn parse_cd_descriptor(input_path: &Path) -> Result<CdToc, DiscError> {
    let parent_dir = input_path
        .parent()
        .unwrap_or_else(|| Path::new("."))
        .to_path_buf();
    let ext = input_path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("")
        .to_ascii_lowercase();

    match ext.as_str() {
        "cue" => {
            let content = std::fs::read_to_string(input_path)?;
            CueSheet::parse(&content, Some(&parent_dir))
        }
        "toc" => {
            let content = std::fs::read_to_string(input_path)?;
            CdrdaoToc::parse(&content)
        }
        "gdi" => {
            let content = std::fs::read_to_string(input_path)?;
            GdiSheet::parse(&content, Some(&parent_dir))
        }
        "ccd" => {
            let content = std::fs::read_to_string(input_path)?;
            let sheet = CcdSheet::parse(&content)?;
            let mut img_path = input_path.with_extension("img");
            if !img_path.is_file() {
                let upper = input_path.with_extension("IMG");
                if upper.is_file() {
                    img_path = upper;
                } else {
                    return Err(DiscError::Io(std::io::Error::new(
                        std::io::ErrorKind::NotFound,
                        format!("CloneCD image payload not found: '{}'", img_path.display()),
                    )));
                }
            }
            let img_file_name = img_path
                .file_name()
                .and_then(|n| n.to_str())
                .map(|s| s.to_string())
                .ok_or_else(|| {
                    DiscError::InvalidCcd("invalid file name for CCD image".to_string())
                })?;
            sheet.to_toc(Some(&parent_dir), Some(&img_file_name))
        }
        "mds" => MdsDisc::parse_file(input_path),
        "nrg" => NrgDisc::parse_file(input_path),
        other => Err(DiscError::InvalidMetadata(format!(
            "unsupported CD descriptor format '.{}'",
            other
        ))),
    }
}