chdlady-disc 0.2.2

Disc geometries and physical media formats for chdlady
//! DVD ISO media image creation pipeline.

use super::common::DEFAULT_HD_CODECS;
use crate::error::DiscError;
use crate::types::DVD_METADATA_TAG;
use chdlady_core::{
    ChdFile, ChdWriteConfig, ChdWriter, CreateResult, MetadataItem, ProgressStatus,
    CHD_MDFLAGS_CHECKSUM,
};
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;

/// Options for creating a DVD ISO CHD image.
#[derive(Debug, Clone, Default)]
pub struct CreateDvdOptions {
    /// Starting byte offset in the input file.
    pub start_byte: Option<u64>,
    /// Starting hunk index in the input file.
    pub start_hunk: Option<u64>,
    /// Total bytes to write.
    pub length_bytes: Option<u64>,
    /// Total hunks to write.
    pub length_hunks: Option<u64>,
    /// Hunk size in bytes (defaults to 4096 = 2 * 2048).
    pub hunk_size: Option<u32>,
    /// Up to 4 FourCC compression codecs.
    pub codecs: Option<[u32; 4]>,
    /// Optional path to parent CHD for differential container.
    pub parent_path: Option<PathBuf>,
    /// Optional limit on worker threads for parallel compression.
    pub num_processors: Option<usize>,
    /// Optional shared atomic flag for cooperative cancellation.
    pub cancel_token: Option<Arc<AtomicBool>>,
}

/// Creates a CHD v5 container from a DVD ISO image file.
pub fn create_dvd<F>(
    input_path: &Path,
    output_path: &Path,
    options: &CreateDvdOptions,
    mut progress: F,
) -> Result<CreateResult, DiscError>
where
    F: FnMut(ProgressStatus),
{
    let mut input_file = File::open(input_path)?;
    let total_size = input_file.metadata()?.len();

    let unit_size = 2048u32;
    let hunk_size = options.hunk_size.unwrap_or(4096);

    let start = match (options.start_byte, options.start_hunk) {
        (Some(b), _) => b,
        (None, Some(h)) => h * hunk_size as u64,
        (None, None) => 0,
    };
    let length = match (options.length_bytes, options.length_hunks) {
        (Some(b), _) => b,
        (None, Some(h)) => h * hunk_size as u64,
        (None, None) => total_size.saturating_sub(start),
    };

    let end = start
        .checked_add(length)
        .ok_or_else(|| DiscError::InvalidMetadata("specified range overflows u64".into()))?;
    if end > total_size {
        return Err(DiscError::InvalidMetadata(
            "specified range exceeds input file size".into(),
        ));
    }
    if !length.is_multiple_of(unit_size as u64) {
        return Err(DiscError::InvalidMetadata(format!(
            "data length {} is not divisible by DVD sector size 2048",
            length
        )));
    }

    let (parent_map, parent_sha1) = if let Some(ref p) = options.parent_path {
        let pf = File::open(p)?;
        let mut parent_chd = ChdFile::open(pf)?;
        let p_sha1 = parent_chd.header().sha1;
        let map = ChdWriter::build_parent_map(&mut parent_chd, hunk_size, unit_size)?;
        (Some(map), p_sha1)
    } else {
        (None, [0u8; 20])
    };

    let compressors = options.codecs.unwrap_or(DEFAULT_HD_CODECS);
    let config = ChdWriteConfig {
        logical_bytes: length,
        hunk_bytes: hunk_size,
        unit_bytes: unit_size,
        compressors,
        parent_sha1,
        num_processors: options.num_processors,
        cancel_token: options.cancel_token.clone(),
    };

    input_file.seek(SeekFrom::Start(start))?;
    let mut reader = BufReader::with_capacity(1024 * 1024, input_file.take(length));

    let dvd_meta = MetadataItem {
        tag: DVD_METADATA_TAG,
        flags: CHD_MDFLAGS_CHECKSUM,
        value: vec![0], // Empty string with null byte
    };

    let output_file = File::create(output_path)?;
    let mut writer = BufWriter::new(output_file);

    let result = ChdWriter::write_chd_with_parent(
        &mut reader,
        &mut writer,
        &config,
        &[dvd_meta],
        parent_map.as_ref(),
        &mut progress,
    );
    match result {
        Ok(res) => Ok(res),
        Err(e) => {
            let _ = std::fs::remove_file(output_path);
            Err(DiscError::Chd(e))
        }
    }
}