chdlady-core 0.1.0

Core container manipulation for CHD v5 format
//! Core container structures, codecs, and metadata for CHD v5 format.

#![deny(missing_docs)]
#![deny(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

/// CD-ROM Reed-Solomon Error Correction Code (ECC) tables and algorithms.
pub mod cd_ecc;
/// CHD file operations, decompression, and integrity verification.
pub mod chdfile;
/// Codec decompression implementations for CHD v5.
pub mod codecs;
/// CRC-16-CCITT calculations.
pub mod crc16;
/// Error definitions for CHD processing.
pub mod error;
/// Header structures and serialization for CHD v5.
pub mod header;
/// Hunk map table definitions and decompression.
pub mod map;
/// Linked-list metadata storage and overall SHA-1 hashing.
pub mod metadata;
/// Structured progress and operation phase tracking.
pub mod progress;
/// CHD v5 container serializer and concurrent compression engine.
pub mod writer;

/// Bytes per raw CD sector frame data (2352 bytes).
pub const CD_SECTOR_SIZE: usize = 2352;
/// Bytes per CD subcode channel frame (96 bytes).
pub const CD_SUBCODE_SIZE: usize = 96;
/// Total bytes per complete CHD optical frame (2448 bytes).
pub const CD_FRAME_SIZE: usize = CD_SECTOR_SIZE + CD_SUBCODE_SIZE;
/// Maximum allowed hunk size (64 MiB) to guard against corrupted container allocation bombs.
pub const MAX_HUNK_BYTES: u32 = 64 * 1024 * 1024;

pub use cd_ecc::{ecc_generate, ecc_verify, CD_SYNC_HEADER};
pub use chdfile::{ChdFile, ChdInfo, ChdParentReader, VerifyResult};
pub use codecs::decompress_hunk;
pub use crc16::crc16;
pub use error::ChdError;
pub use header::{
    ChdHeader, CHD_V3_HEADER_SIZE, CHD_V3_VERSION, CHD_V4_HEADER_SIZE, CHD_V4_VERSION,
    CHD_V5_HEADER_SIZE, CHD_V5_SIGNATURE, CHD_V5_VERSION,
};
pub use map::{
    read_v34_map, read_v5_map, write_v5_map, write_v5_uncompressed_map, HunkType, MapEntry,
};
pub use metadata::{
    compute_overall_sha1, delete_metadata_entry, read_all_metadata, write_metadata_entry,
    MetadataEntry, MetadataIterator, CHD_MDFLAGS_CHECKSUM,
};
pub use progress::{OperationPhase, ProgressStatus};
pub use writer::{ChdWriteConfig, ChdWriter, CreateResult, MetadataItem};

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::io::{Cursor, Seek};
    use std::process::Command;

    #[test]
    fn test_header_round_trip() {
        let header = ChdHeader {
            tag: *CHD_V5_SIGNATURE,
            length: CHD_V5_HEADER_SIZE,
            version: CHD_V5_VERSION,
            compressors: [
                u32::from_be_bytes(*b"zlib"),
                u32::from_be_bytes(*b"lzma"),
                0,
                0,
            ],
            logical_bytes: 1048576,
            map_offset: 200,
            meta_offset: 500,
            hunk_bytes: 4096,
            unit_bytes: 512,
            raw_sha1: [1u8; 20],
            sha1: [2u8; 20],
            parent_sha1: [0u8; 20],
        };

        let mut buffer = Cursor::new(vec![0u8; 124]);
        header.write_v5(&mut buffer).expect("write header");

        buffer.set_position(0);
        let parsed = ChdHeader::read_v5(&mut buffer).expect("read header");
        assert_eq!(parsed, header);
        assert_eq!(parsed.hunk_count(), 256);
        assert_eq!(parsed.unit_count(), 2048);
        assert!(parsed.is_compressed());
    }

    #[test]
    fn test_crc16_known_vector() {
        let data = b"123456789";
        let crc = crc16(data, 0xffff);
        assert_eq!(crc, 0x29b1);
    }

    #[test]
    fn test_parse_chdman_generated_raw_and_compressed() {
        let temp_dir = std::env::temp_dir().join(format!("chd_test_{}", std::process::id()));
        let _ = fs::create_dir_all(&temp_dir);

        let bin_path = temp_dir.join("test_src.bin");
        let chd_raw_path = temp_dir.join("test_raw.chd");
        let chd_zlib_path = temp_dir.join("test_zlib.chd");

        let mut data = vec![0u8; 16384];
        for (i, byte) in data.iter_mut().enumerate() {
            *byte = (i % 251) as u8;
        }
        fs::write(&bin_path, &data).expect("write test bin");

        let status_raw = Command::new("chdman")
            .args([
                "createraw",
                "-i",
                bin_path.to_str().unwrap(),
                "-o",
                chd_raw_path.to_str().unwrap(),
                "-hs",
                "4096",
                "-us",
                "4096",
                "-c",
                "none",
                "-f",
            ])
            .status()
            .expect("run chdman createraw none");
        assert!(status_raw.success());

        let status_meta = Command::new("chdman")
            .args([
                "addmeta",
                "-i",
                chd_raw_path.to_str().unwrap(),
                "-t",
                "TEST",
                "-vt",
                "test_metadata_payload",
            ])
            .status()
            .expect("run chdman addmeta");
        assert!(status_meta.success());

        let mut file_raw = fs::File::open(&chd_raw_path).expect("open raw chd");
        let header_raw = ChdHeader::read_v5(&mut file_raw).expect("read v5 raw header");
        assert_eq!(header_raw.logical_bytes, 16384);
        assert_eq!(header_raw.hunk_bytes, 4096);
        assert_eq!(header_raw.hunk_count(), 4);
        assert!(!header_raw.is_compressed());

        let map_raw = read_v5_map(&mut file_raw, &header_raw).expect("read raw map");
        assert_eq!(map_raw.len(), 4);
        for entry in &map_raw {
            assert_eq!(entry.hunk_type, HunkType::Uncompressed);
            assert_eq!(entry.length, 4096);
        }

        let metadata_raw =
            read_all_metadata(&mut file_raw, header_raw.meta_offset).expect("read all metadata");
        assert_eq!(metadata_raw.len(), 1);
        assert_eq!(metadata_raw[0].tag_string(), "TEST");
        assert!(metadata_raw[0].value.starts_with(b"test_metadata_payload"));

        // Test ChdFile on raw uncompressed
        file_raw.seek(std::io::SeekFrom::Start(0)).unwrap();
        let mut chd_raw = ChdFile::open(file_raw).expect("open chd raw");
        let mut raw_decompressed = vec![0u8; 16384];
        chd_raw
            .read_bytes(0, &mut raw_decompressed)
            .expect("read_bytes raw");
        assert_eq!(raw_decompressed, data);

        let verify_raw = chd_raw.verify(|_| {}).expect("verify raw");
        assert!(!verify_raw.is_compressed);
        assert!(verify_raw.raw_sha1_matched);
        assert!(verify_raw.overall_sha1_matched);

        let info_raw = chd_raw.info().expect("info raw");
        assert_eq!(info_raw.version, 5);
        assert_eq!(info_raw.logical_bytes, 16384);
        assert_eq!(info_raw.metadata_count, 1);

        let status_zlib = Command::new("chdman")
            .args([
                "createraw",
                "-i",
                bin_path.to_str().unwrap(),
                "-o",
                chd_zlib_path.to_str().unwrap(),
                "-hs",
                "4096",
                "-us",
                "4096",
                "-c",
                "zlib",
                "-f",
            ])
            .status()
            .expect("run chdman createraw zlib");
        assert!(status_zlib.success());

        let mut file_zlib = fs::File::open(&chd_zlib_path).expect("open zlib chd");
        let header_zlib = ChdHeader::read_v5(&mut file_zlib).expect("read v5 zlib header");
        assert_eq!(header_zlib.logical_bytes, 16384);
        assert_eq!(header_zlib.hunk_bytes, 4096);
        assert_eq!(header_zlib.hunk_count(), 4);
        assert!(header_zlib.is_compressed());

        let map_zlib = read_v5_map(&mut file_zlib, &header_zlib).expect("read zlib map");
        assert_eq!(map_zlib.len(), 4);

        // Test ChdFile on zlib
        file_zlib.seek(std::io::SeekFrom::Start(0)).unwrap();
        let mut chd_zlib = ChdFile::open(file_zlib).expect("open chd zlib");
        let mut decompressed = vec![0u8; 16384];
        chd_zlib
            .read_bytes(0, &mut decompressed)
            .expect("read_bytes zlib");
        assert_eq!(decompressed, data);

        let verify_zlib = chd_zlib.verify(|_| {}).expect("verify zlib");
        assert!(verify_zlib.raw_sha1_matched);
        assert!(verify_zlib.overall_sha1_matched);

        // Test ChdFile on lzma
        let chd_lzma_path = temp_dir.join("test_lzma.chd");
        let status_lzma = Command::new("chdman")
            .args([
                "createraw",
                "-i",
                bin_path.to_str().unwrap(),
                "-o",
                chd_lzma_path.to_str().unwrap(),
                "-hs",
                "4096",
                "-us",
                "4096",
                "-c",
                "lzma",
                "-f",
            ])
            .status()
            .expect("run chdman createraw lzma");
        assert!(status_lzma.success());

        let file_lzma = fs::File::open(&chd_lzma_path).expect("open lzma chd");
        let mut chd_lzma = ChdFile::open(file_lzma).expect("open chd lzma");
        decompressed.fill(0);
        chd_lzma
            .read_bytes(0, &mut decompressed)
            .expect("read_bytes lzma");
        assert_eq!(decompressed, data);

        let verify_lzma = chd_lzma.verify(|_| {}).expect("verify lzma");
        assert!(verify_lzma.raw_sha1_matched);
        assert!(verify_lzma.overall_sha1_matched);

        // Test ChdFile on huff
        let chd_huff_path = temp_dir.join("test_huff.chd");
        let status_huff = Command::new("chdman")
            .args([
                "createraw",
                "-i",
                bin_path.to_str().unwrap(),
                "-o",
                chd_huff_path.to_str().unwrap(),
                "-hs",
                "4096",
                "-us",
                "4096",
                "-c",
                "huff",
                "-f",
            ])
            .status()
            .expect("run chdman createraw huff");
        assert!(status_huff.success());

        let file_huff = fs::File::open(&chd_huff_path).expect("open huff chd");
        let mut chd_huff = ChdFile::open(file_huff).expect("open chd huff");
        decompressed.fill(0);
        chd_huff
            .read_bytes(0, &mut decompressed)
            .expect("read_bytes huff");
        assert_eq!(decompressed, data);

        let verify_huff = chd_huff.verify(|_| {}).expect("verify huff");
        assert!(verify_huff.raw_sha1_matched);
        assert!(verify_huff.overall_sha1_matched);

        // Test ChdFile on zstd
        #[cfg(feature = "zstd")]
        {
            let chd_zstd_path = temp_dir.join("test_zstd.chd");
            let status_zstd = Command::new("chdman")
                .args([
                    "createraw",
                    "-i",
                    bin_path.to_str().unwrap(),
                    "-o",
                    chd_zstd_path.to_str().unwrap(),
                    "-hs",
                    "4096",
                    "-us",
                    "4096",
                    "-c",
                    "zstd",
                    "-f",
                ])
                .status()
                .expect("run chdman createraw zstd");
            assert!(status_zstd.success());

            let file_zstd = fs::File::open(&chd_zstd_path).expect("open zstd chd");
            let mut chd_zstd = ChdFile::open(file_zstd).expect("open chd zstd");
            decompressed.fill(0);
            chd_zstd
                .read_bytes(0, &mut decompressed)
                .expect("read_bytes zstd");
            assert_eq!(decompressed, data);

            let verify_zstd = chd_zstd.verify(|_| {}).expect("verify zstd");
            assert!(verify_zstd.raw_sha1_matched);
            assert!(verify_zstd.overall_sha1_matched);
        }

        // Test CD codecs: cdzl, cdlz, cdzs, cdfl
        let cue_path = temp_dir.join("test_cd.cue");
        let cd_bin_path = temp_dir.join("test_cd.bin");
        let num_cd_frames = 16;
        let mut cd_data = vec![0u8; num_cd_frames * 2352];
        for (i, b) in cd_data.iter_mut().enumerate() {
            *b = (i % 256) as u8;
        }
        fs::write(&cd_bin_path, &cd_data).expect("write cd bin");
        fs::write(
            &cue_path,
            "FILE \"test_cd.bin\" BINARY\n  TRACK 01 AUDIO\n    INDEX 01 00:00:00\n",
        )
        .expect("write cue");

        #[cfg(feature = "zstd")]
        let cd_codecs = ["cdzl", "cdlz", "cdzs", "cdfl"];
        #[cfg(not(feature = "zstd"))]
        let cd_codecs = ["cdzl", "cdlz", "cdfl"];

        for codec in cd_codecs {
            let cd_chd_path = temp_dir.join(format!("test_{}.chd", codec));
            let status = Command::new("chdman")
                .args([
                    "createcd",
                    "-i",
                    cue_path.to_str().unwrap(),
                    "-o",
                    cd_chd_path.to_str().unwrap(),
                    "-c",
                    codec,
                    "-f",
                ])
                .status()
                .expect("run chdman createcd");
            assert!(status.success(), "createcd failed for codec {}", codec);

            let file_cd = fs::File::open(&cd_chd_path).expect("open cd chd");
            let mut chd_cd = ChdFile::open(file_cd).expect("open ChdFile cd");
            let verify_cd = chd_cd.verify(|_| {}).expect("verify cd");
            assert!(
                verify_cd.raw_sha1_matched,
                "raw sha1 mismatch for codec {}",
                codec
            );
            assert!(
                verify_cd.overall_sha1_matched,
                "overall sha1 mismatch for codec {}",
                codec
            );
        }

        let _ = fs::remove_dir_all(&temp_dir);
    }
}