#![deny(missing_docs)]
#![deny(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub mod cd_ecc;
pub mod chdfile;
pub mod codecs;
pub mod crc16;
pub mod error;
pub mod header;
pub mod map;
pub mod metadata;
pub mod progress;
pub mod writer;
pub const CD_SECTOR_SIZE: usize = 2352;
pub const CD_SUBCODE_SIZE: usize = 96;
pub const CD_FRAME_SIZE: usize = CD_SECTOR_SIZE + CD_SUBCODE_SIZE;
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"));
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);
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);
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);
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);
#[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);
}
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);
}
}