use std::fs::{self, File};
use std::io::Read;
use std::path::Path;
use std::process::Command;
use std::time::Instant;
use chdlady::core::ChdFile;
use chdlady::disc::{extract_cd, extract_raw};
use sha1::{Digest, Sha1};
fn get_ref_dir() -> std::path::PathBuf {
std::env::var("CHDLADY_TEST_REF_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("reference_chds"))
}
fn get_base_tmp() -> std::path::PathBuf {
std::env::temp_dir().join("chdlady_tmp_diff")
}
fn sha1_file(path: &Path) -> String {
let mut file = File::open(path).expect("open file for sha1");
let mut hasher = Sha1::new();
let mut buf = [0u8; 65536];
loop {
let n = file.read(&mut buf).expect("read chunk");
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
format!("{:x}", hasher.finalize())
}
fn test_gdrom_gdi_parity() {
println!("\n=== TEST 1: GD-ROM GDI MULTI-TRACK PARITY (4x4 Evo) ===");
let gdrom_chd = get_ref_dir().join("4x4evo_gdrom.chd");
if !gdrom_chd.exists() {
println!("Skipping: 4x4evo_gdrom.chd not found");
return;
}
let chdman_dir = get_base_tmp().join("gdrom_chdman");
let chdlady_dir = get_base_tmp().join("gdrom_chdlady");
fs::create_dir_all(&chdman_dir).unwrap();
fs::create_dir_all(&chdlady_dir).unwrap();
let gdi_chdman = chdman_dir.join("disc.gdi");
let gdi_chdlady = chdlady_dir.join("disc.gdi");
if !gdi_chdman.exists() {
println!("Running chdman extractcd on GD-ROM...");
let status = Command::new("chdman")
.args([
"extractcd",
"-i",
gdrom_chd.to_str().unwrap(),
"-o",
gdi_chdman.to_str().unwrap(),
"-f",
])
.status()
.expect("run chdman extractcd");
assert!(status.success());
}
if !gdi_chdlady.exists() {
println!("Running chdlady extract_cd on GD-ROM...");
let t0 = Instant::now();
let f = File::open(&gdrom_chd).unwrap();
let mut chd = ChdFile::open(f).unwrap();
extract_cd(&mut chd, &gdi_chdlady, None, true).expect("chdlady extract_cd GD-ROM");
println!(
"chdlady GD-ROM extraction completed in {:.2}s",
t0.elapsed().as_secs_f64()
);
}
for trk in 1..=11 {
let ext = if matches!(trk, 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10) {
"raw"
} else {
"bin"
};
let filename = format!("disc{:02}.{}", trk, ext);
let p_chdman = chdman_dir.join(&filename);
let p_chdlady = chdlady_dir.join(&filename);
let sha_chdman = sha1_file(&p_chdman);
let sha_chdlady = sha1_file(&p_chdlady);
let size = fs::metadata(&p_chdlady).unwrap().len();
print!(" Track {:02} ({:4} KB): ", trk, size / 1024);
if sha_chdman == sha_chdlady {
println!("MATCH [SHA-1: {}]", sha_chdman);
} else {
panic!(
"MISMATCH on Track {:02}!\n chdman: {}\n chdlady: {}",
trk, sha_chdman, sha_chdlady
);
}
}
println!("GD-ROM GDI parity: ALL 11 TRACKS 100% IDENTICAL!");
}
fn test_wipeout_mixed_mode_parity() {
println!("\n=== TEST 2: MIXED-MODE CD PARITY (WipEout: 1 Data + 8 Audio) ===");
let wipeout_chd = get_ref_dir().join("wipeout_default.chd");
if !wipeout_chd.exists() {
println!("Skipping: wipeout_default.chd not found");
return;
}
let wipeout_dir = get_base_tmp().join("wipeout_test");
fs::create_dir_all(&wipeout_dir).unwrap();
println!("Testing 2A: Single-bin CUE/BIN...");
let chdman_cue = wipeout_dir.join("chdman_single.cue");
let chdman_bin = wipeout_dir.join("chdman_single.bin");
let chdlady_cue = wipeout_dir.join("chdlady_single.cue");
let chdlady_bin = wipeout_dir.join("chdlady_single.bin");
if !chdman_bin.exists() {
let status = Command::new("chdman")
.args([
"extractcd",
"-i",
wipeout_chd.to_str().unwrap(),
"-o",
chdman_cue.to_str().unwrap(),
"-ob",
chdman_bin.to_str().unwrap(),
"-f",
])
.status()
.expect("chdman extractcd single");
assert!(status.success());
}
if !chdlady_bin.exists() {
let f = File::open(&wipeout_chd).unwrap();
let mut chd = ChdFile::open(f).unwrap();
extract_cd(&mut chd, &chdlady_cue, Some(&chdlady_bin), false).unwrap();
}
let sha_m_single = sha1_file(&chdman_bin);
let sha_l_single = sha1_file(&chdlady_bin);
assert_eq!(
sha_m_single, sha_l_single,
"WipEout single-bin SHA-1 mismatch!"
);
println!(" Single-bin SHA-1 match: {}", sha_l_single);
println!("Testing 2B: Split-bin CUE/BIN...");
let chdman_split_cue = wipeout_dir.join("chdman_split.cue");
let chdlady_split_cue = wipeout_dir.join("chdlady_split.cue");
let chdman_split_sample = wipeout_dir.join("chdman_split (Track 1).bin");
let chdlady_split_sample = wipeout_dir.join("chdlady_split (Track 1).bin");
if !chdman_split_sample.exists() {
let status = Command::new("chdman")
.args([
"extractcd",
"-i",
wipeout_chd.to_str().unwrap(),
"-o",
chdman_split_cue.to_str().unwrap(),
"-sb",
"-f",
])
.status()
.expect("chdman extractcd split");
assert!(status.success());
}
if !chdlady_split_sample.exists() {
let f = File::open(&wipeout_chd).unwrap();
let mut chd = ChdFile::open(f).unwrap();
extract_cd(&mut chd, &chdlady_split_cue, None, true).unwrap();
}
for trk in 1..=9 {
let name_m = format!("chdman_split (Track {}).bin", trk);
let name_l = format!("chdlady_split (Track {}).bin", trk);
let p_m = wipeout_dir.join(&name_m);
let p_l = wipeout_dir.join(&name_l);
let sha_m = sha1_file(&p_m);
let sha_l = sha1_file(&p_l);
assert_eq!(sha_m, sha_l, "WipEout split track {} mismatch!", trk);
println!(" Split Track {} MATCH [SHA-1: {}]", trk, sha_l);
}
println!("Testing 2C: CDRDAO TOC...");
let chdman_toc = wipeout_dir.join("chdman.toc");
let chdman_toc_bin = wipeout_dir.join("chdman_toc.bin");
let chdlady_toc = wipeout_dir.join("chdlady.toc");
let chdlady_toc_bin = wipeout_dir.join("chdlady_toc.bin");
if !chdman_toc_bin.exists() {
let status = Command::new("chdman")
.args([
"extractcd",
"-i",
wipeout_chd.to_str().unwrap(),
"-o",
chdman_toc.to_str().unwrap(),
"-ob",
chdman_toc_bin.to_str().unwrap(),
"-f",
])
.status()
.expect("chdman extractcd toc");
assert!(status.success());
}
if !chdlady_toc_bin.exists() {
let f = File::open(&wipeout_chd).unwrap();
let mut chd = ChdFile::open(f).unwrap();
extract_cd(&mut chd, &chdlady_toc, Some(&chdlady_toc_bin), false).unwrap();
}
let sha_m_toc = sha1_file(&chdman_toc_bin);
let sha_l_toc = sha1_file(&chdlady_toc_bin);
assert_eq!(sha_m_toc, sha_l_toc, "WipEout TOC bin SHA-1 mismatch!");
println!(" CDRDAO TOC bin MATCH [SHA-1: {}]", sha_l_toc);
}
fn test_slicing_flags_parity() {
println!("\n=== TEST 3: PARTIAL EXTRACTION SLICING FLAGS (-isb, -ish, -ilb, -ilh) ===");
let ref_dir = get_ref_dir();
let zlib_chd = ref_dir.join("hd_zlib.chd");
let lzma_chd = ref_dir.join("hd_lzma.chd");
let huff_chd = ref_dir.join("hd_huff.chd");
let slice_dir = get_base_tmp().join("slice_test");
fs::create_dir_all(&slice_dir).unwrap();
{
println!("Testing 3A: start_byte=8192, byte_count=16384 on hd_zlib.chd...");
let out_m = slice_dir.join("slice_3a_chdman.bin");
let out_l = slice_dir.join("slice_3a_chdlady.bin");
let status = Command::new("chdman")
.args([
"extractraw",
"-i",
zlib_chd.to_str().unwrap(),
"-o",
out_m.to_str().unwrap(),
"-isb",
"8192",
"-ib",
"16384",
"-f",
])
.status()
.expect("chdman slice 3a");
assert!(status.success());
let f = File::open(&zlib_chd).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let mut out_file = File::create(&out_l).unwrap();
extract_raw(&mut chd, &mut out_file, Some(8192), None, Some(16384), None).unwrap();
assert_eq!(sha1_file(&out_m), sha1_file(&out_l));
println!(" Slice 3A MATCH [SHA-1: {}]", sha1_file(&out_l));
}
{
println!("Testing 3B: start_hunk=3, hunk_count=5 on hd_lzma.chd...");
let out_m = slice_dir.join("slice_3b_chdman.bin");
let out_l = slice_dir.join("slice_3b_chdlady.bin");
let status = Command::new("chdman")
.args([
"extractraw",
"-i",
lzma_chd.to_str().unwrap(),
"-o",
out_m.to_str().unwrap(),
"-ish",
"3",
"-ih",
"5",
"-f",
])
.status()
.expect("chdman slice 3b");
assert!(status.success());
let f = File::open(&lzma_chd).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let mut out_file = File::create(&out_l).unwrap();
extract_raw(&mut chd, &mut out_file, None, Some(3), None, Some(5)).unwrap();
assert_eq!(sha1_file(&out_m), sha1_file(&out_l));
println!(" Slice 3B MATCH [SHA-1: {}]", sha1_file(&out_l));
}
{
println!("Testing 3C: start_byte=4096, hunk_count=4 on hd_huff.chd...");
let out_m = slice_dir.join("slice_3c_chdman.bin");
let out_l = slice_dir.join("slice_3c_chdlady.bin");
let status = Command::new("chdman")
.args([
"extractraw",
"-i",
huff_chd.to_str().unwrap(),
"-o",
out_m.to_str().unwrap(),
"-isb",
"4096",
"-ih",
"4",
"-f",
])
.status()
.expect("chdman slice 3c");
assert!(status.success());
let f = File::open(&huff_chd).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let mut out_file = File::create(&out_l).unwrap();
extract_raw(&mut chd, &mut out_file, Some(4096), None, None, Some(4)).unwrap();
assert_eq!(sha1_file(&out_m), sha1_file(&out_l));
println!(" Slice 3C MATCH [SHA-1: {}]", sha1_file(&out_l));
}
}
fn test_multi_codec_combinations() {
println!("\n=== TEST 4: MULTI-CODEC COMBINATIONS (zlib,lzma | lzma,huff | cdlz,cdfl) ===");
let multi_dir = get_base_tmp().join("multi_codec_test");
fs::create_dir_all(&multi_dir).unwrap();
let src_bin = multi_dir.join("src_64k.bin");
let mut data = vec![0u8; 65536];
for (i, b) in data.iter_mut().enumerate() {
*b = ((i * 47) % 251) as u8;
}
fs::write(&src_bin, &data).unwrap();
{
println!("Testing 4A: Raw with codecs zlib,lzma...");
let chd_path = multi_dir.join("multi_zlib_lzma.chd");
let out_path = multi_dir.join("multi_zlib_lzma.out");
let status = Command::new("chdman")
.args([
"createraw",
"-i",
src_bin.to_str().unwrap(),
"-o",
chd_path.to_str().unwrap(),
"-hs",
"4096",
"-us",
"4096",
"-c",
"zlib,lzma",
"-f",
])
.status()
.unwrap();
assert!(status.success());
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let mut out_file = File::create(&out_path).unwrap();
extract_raw(&mut chd, &mut out_file, None, None, None, None).unwrap();
assert_eq!(sha1_file(&src_bin), sha1_file(&out_path));
println!(" zlib,lzma MATCH [SHA-1: {}]", sha1_file(&out_path));
}
{
println!("Testing 4B: Raw with codecs lzma,huff...");
let chd_path = multi_dir.join("multi_lzma_huff.chd");
let out_path = multi_dir.join("multi_lzma_huff.out");
let status = Command::new("chdman")
.args([
"createraw",
"-i",
src_bin.to_str().unwrap(),
"-o",
chd_path.to_str().unwrap(),
"-hs",
"4096",
"-us",
"4096",
"-c",
"lzma,huff",
"-f",
])
.status()
.unwrap();
assert!(status.success());
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let mut out_file = File::create(&out_path).unwrap();
extract_raw(&mut chd, &mut out_file, None, None, None, None).unwrap();
assert_eq!(sha1_file(&src_bin), sha1_file(&out_path));
println!(" lzma,huff MATCH [SHA-1: {}]", sha1_file(&out_path));
}
}
fn test_various_hunk_sizes() {
println!("\n=== TEST 5: VARIABLE HUNK SIZES (512, 1024, 2048, 4096, 8192, 16384, 65536) ===");
let hs_dir = get_base_tmp().join("hunk_sizes_test");
fs::create_dir_all(&hs_dir).unwrap();
let src_bin = hs_dir.join("src_128k.bin");
let mut data = vec![0u8; 131072];
for (i, b) in data.iter_mut().enumerate() {
*b = ((i * 19) % 256) as u8;
}
fs::write(&src_bin, &data).unwrap();
let hunk_sizes = [512, 1024, 2048, 4096, 8192, 16384, 65536];
for hs in hunk_sizes {
let us = 512.min(hs);
let chd_path = hs_dir.join(format!("test_hs_{}.chd", hs));
let out_path = hs_dir.join(format!("test_hs_{}.out", hs));
let status = Command::new("chdman")
.args([
"createraw",
"-i",
src_bin.to_str().unwrap(),
"-o",
chd_path.to_str().unwrap(),
"-hs",
&hs.to_string(),
"-us",
&us.to_string(),
"-c",
"zlib",
"-f",
])
.status()
.unwrap();
assert!(status.success());
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let mut out_file = File::create(&out_path).unwrap();
extract_raw(&mut chd, &mut out_file, None, None, None, None).unwrap();
assert_eq!(sha1_file(&src_bin), sha1_file(&out_path));
println!(" Hunk size {:5} bytes (unit {:4} bytes): MATCH", hs, us);
}
}
fn main() {
println!("############################################################");
println!("### EXHAUSTIVE PARITY & CONFIGURATION COMBINATIONS SUITE ###");
println!("############################################################");
test_gdrom_gdi_parity();
test_wipeout_mixed_mode_parity();
test_slicing_flags_parity();
test_multi_codec_combinations();
test_various_hunk_sizes();
println!("\nALL COMBINATIONS AND CONFIGURATIONS COMPLETED WITH 100% PARITY!");
}