chdlady-disc 0.2.2

Disc geometries and physical media formats for chdlady
//! Raw binary media image creation pipeline.

use super::common::DEFAULT_RAW_CODECS;
use crate::error::DiscError;
use chdlady_core::{ChdFile, ChdWriteConfig, ChdWriter, CreateResult, ProgressStatus};
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 raw CHD image.
#[derive(Debug, Clone, Default)]
pub struct CreateRawOptions {
    /// 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.
    pub hunk_size: Option<u32>,
    /// Unit size in bytes (required).
    pub unit_size: 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 raw binary input file.
pub fn create_raw<F>(
    input_path: &Path,
    output_path: &Path,
    options: &CreateRawOptions,
    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 = options.unit_size;
    if unit_size == 0 {
        return Err(DiscError::InvalidMetadata(
            "unit size must be non-zero".into(),
        ));
    }
    let hunk_size = options
        .hunk_size
        .unwrap_or_else(|| ((4096 / unit_size) * unit_size).max(unit_size));

    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 unit size {}",
            length, unit_size
        )));
    }

    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_RAW_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 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,
        &[],
        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))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_create_raw_cancellation() {
        let temp_dir =
            std::env::temp_dir().join(format!("chdlady_raw_cancel_{}", std::process::id()));
        let _ = std::fs::create_dir_all(&temp_dir);
        let raw_path = temp_dir.join("test.bin");
        let out_path = temp_dir.join("output.chd");

        let test_data = vec![0x42u8; 65536];
        std::fs::write(&raw_path, &test_data).unwrap();

        let cancel_token = Arc::new(AtomicBool::new(true));
        let options = CreateRawOptions {
            unit_size: 512,
            cancel_token: Some(cancel_token),
            ..Default::default()
        };

        let result = create_raw(&raw_path, &out_path, &options, |_| {});
        match result {
            Err(DiscError::Chd(chdlady_core::ChdError::Cancelled)) => {}
            other => panic!("expected Err(DiscError::Chd(Cancelled)), got {:?}", other),
        }

        assert!(
            !out_path.exists(),
            "incomplete output CHD file should be deleted on cancellation"
        );
        let _ = std::fs::remove_dir_all(&temp_dir);
    }
}