use crate::ccd::CcdSheet;
use crate::cue::CueSheet;
use crate::error::DiscError;
use crate::gdi::GdiSheet;
use crate::hd::HardDiskGeo;
use crate::mds::MdsDisc;
use crate::nrg::NrgDisc;
use crate::metadata::parse_toc_from_metadata;
use crate::sub::SubFrame;
use crate::subcode::swap_audio_endianness;
use crate::toc::CdrdaoToc;
use crate::types::{CdSubcodeType, CdToc, CD_TOTAL_FRAME_SIZE, HARD_DISK_METADATA_TAG};
use chdlady_core::{ChdFile, OperationPhase, ProgressStatus};
use std::fs::File;
use std::io::{BufWriter, Read, Seek, Write};
use std::path::Path;
const CD_TRACK_PADDING: u32 = 4;
pub fn extract_raw<R: Read + Seek, W: Write>(
chd: &mut ChdFile<R>,
writer: &mut W,
start_byte: Option<u64>,
start_hunk: Option<u64>,
byte_count: Option<u64>,
hunk_count: Option<u64>,
) -> Result<u64, DiscError> {
extract_raw_with_progress(
chd,
writer,
start_byte,
start_hunk,
byte_count,
hunk_count,
|_| {},
)
}
pub fn extract_raw_with_progress<R: Read + Seek, W: Write, F>(
chd: &mut ChdFile<R>,
writer: &mut W,
start_byte: Option<u64>,
start_hunk: Option<u64>,
byte_count: Option<u64>,
hunk_count: Option<u64>,
progress: F,
) -> Result<u64, DiscError>
where
F: FnMut(ProgressStatus),
{
let header = *chd.header();
let hunk_bytes = header.hunk_bytes as u64;
let offset = match (start_byte, start_hunk) {
(Some(b), _) => b,
(None, Some(h)) => h * hunk_bytes,
(None, None) => 0,
};
let total_len = match (byte_count, hunk_count) {
(Some(b), _) => b,
(None, Some(h)) => h * hunk_bytes,
(None, None) => header.logical_bytes.saturating_sub(offset),
};
chd.extract_range_parallel_with_progress(offset, total_len, writer, progress)?;
writer.flush()?;
Ok(total_len)
}
pub fn extract_hd<R: Read + Seek, W: Write>(
chd: &mut ChdFile<R>,
writer: &mut W,
start_byte: Option<u64>,
start_hunk: Option<u64>,
byte_count: Option<u64>,
hunk_count: Option<u64>,
) -> Result<(u64, Option<HardDiskGeo>), DiscError> {
extract_hd_with_progress(
chd,
writer,
start_byte,
start_hunk,
byte_count,
hunk_count,
|_| {},
)
}
pub fn extract_hd_with_progress<R: Read + Seek, W: Write, F>(
chd: &mut ChdFile<R>,
writer: &mut W,
start_byte: Option<u64>,
start_hunk: Option<u64>,
byte_count: Option<u64>,
hunk_count: Option<u64>,
progress: F,
) -> Result<(u64, Option<HardDiskGeo>), DiscError>
where
F: FnMut(ProgressStatus),
{
let metadata_entries = chd.read_all_metadata()?;
let mut hd_geo = None;
for entry in &metadata_entries {
if entry.metatag == HARD_DISK_METADATA_TAG {
let text = String::from_utf8_lossy(&entry.value);
if let Ok(geo) = HardDiskGeo::parse_metadata(&text) {
hd_geo = Some(geo);
break;
}
}
}
let written = extract_raw_with_progress(
chd, writer, start_byte, start_hunk, byte_count, hunk_count, progress,
)?;
Ok((written, hd_geo))
}
pub fn extract_cd<R: Read + Seek>(
chd: &mut ChdFile<R>,
output_toc_path: &Path,
output_bin_path: Option<&Path>,
split_bin: bool,
) -> Result<CdToc, DiscError> {
extract_cd_with_progress(chd, output_toc_path, output_bin_path, split_bin, |_| {})
}
pub fn extract_cd_with_progress<R: Read + Seek, F>(
chd: &mut ChdFile<R>,
output_toc_path: &Path,
output_bin_path: Option<&Path>,
split_bin: bool,
mut progress: F,
) -> Result<CdToc, DiscError>
where
F: FnMut(ProgressStatus),
{
let metadata_entries = chd.read_all_metadata()?;
let toc = parse_toc_from_metadata(&metadata_entries)?;
let parent_dir = output_toc_path.parent().unwrap_or_else(|| Path::new("."));
let toc_stem = output_toc_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
let toc_ext = output_toc_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("cue")
.to_ascii_lowercase();
let total_frames: u64 = toc.tracks.iter().map(|t| t.frames as u64).sum();
let mut extracted_frames = 0u64;
progress(ProgressStatus {
phase: OperationPhase::Extracting,
current: 0,
total: total_frames,
ratio: None,
});
let mut on_frames = |frames: u32| {
extracted_frames += frames as u64;
progress(ProgressStatus {
phase: OperationPhase::Extracting,
current: extracted_frames,
total: total_frames,
ratio: None,
});
};
match toc_ext.as_str() {
"gdi" => extract_cd_gdi(
chd,
&toc,
output_toc_path,
parent_dir,
toc_stem,
&mut on_frames,
)?,
"toc" => extract_cd_toc(
chd,
&toc,
output_toc_path,
output_bin_path,
parent_dir,
toc_stem,
&mut on_frames,
)?,
"ccd" => extract_cd_ccd(
chd,
&toc,
output_toc_path,
output_bin_path,
parent_dir,
toc_stem,
&mut on_frames,
)?,
"mds" => extract_cd_mds(
chd,
&toc,
output_toc_path,
output_bin_path,
parent_dir,
toc_stem,
&mut on_frames,
)?,
"nrg" => extract_cd_nrg(
chd,
&toc,
output_toc_path,
&mut on_frames,
)?,
_ => extract_cd_cue(
chd,
&toc,
output_toc_path,
output_bin_path,
parent_dir,
toc_stem,
split_bin,
&mut on_frames,
)?,
}
Ok(toc)
}
const EXTRACT_FRAMES_BATCH: usize = 4096;
#[allow(clippy::too_many_arguments)]
fn extract_track_frames<R: Read + Seek, W: Write, P: FnMut(u32)>(
chd: &mut ChdFile<R>,
writer: &mut W,
frame_buf: &mut [u8],
start_frame_idx: u32,
data_frames: u32,
sec_size: usize,
swap_endian: bool,
mut on_frames: P,
) -> Result<(), DiscError> {
let mut frames_remaining = data_frames;
let mut current_frame = start_frame_idx;
while frames_remaining > 0 {
let chunk_frames = (frames_remaining as usize).min(EXTRACT_FRAMES_BATCH);
let read_bytes_count = chunk_frames * CD_TOTAL_FRAME_SIZE;
let offset = current_frame as u64 * CD_TOTAL_FRAME_SIZE as u64;
let mut cursor = std::io::Cursor::new(&mut frame_buf[..read_bytes_count]);
chd.extract_range_parallel(offset, read_bytes_count as u64, &mut cursor)?;
for f in 0..chunk_frames {
let start = f * CD_TOTAL_FRAME_SIZE;
let end = start + sec_size;
let frame_slice = &mut frame_buf[start..end];
if swap_endian {
swap_audio_endianness(&mut frame_slice[..2352.min(sec_size)]);
}
writer.write_all(frame_slice)?;
}
on_frames(chunk_frames as u32);
current_frame += chunk_frames as u32;
frames_remaining -= chunk_frames as u32;
}
Ok(())
}
fn extract_cd_gdi<R: Read + Seek, P: FnMut(u32)>(
chd: &mut ChdFile<R>,
toc: &CdToc,
output_toc_path: &Path,
parent_dir: &Path,
toc_stem: &str,
mut on_frames: P,
) -> Result<(), DiscError> {
let gdi_content = GdiSheet::format(toc, toc_stem);
std::fs::write(output_toc_path, gdi_content)?;
let mut current_frame_idx: u32 = 0;
let mut frame_buf = vec![0u8; EXTRACT_FRAMES_BATCH * CD_TOTAL_FRAME_SIZE];
for track in &toc.tracks {
let is_audio = track.track_type.is_audio();
let ext = if is_audio { "raw" } else { "bin" };
let filename = format!("{}{:02}.{}", toc_stem, track.track_num, ext);
let file_path = parent_dir.join(filename);
let file = File::create(file_path)?;
let mut writer = BufWriter::with_capacity(512 * 1024, file);
let data_frames = track.frames.saturating_sub(track.pad_frames);
let sec_size = track.track_type.sector_size();
extract_track_frames(
chd,
&mut writer,
&mut frame_buf,
current_frame_idx,
data_frames,
sec_size,
is_audio,
&mut on_frames,
)?;
writer.flush()?;
current_frame_idx += track.frames.div_ceil(CD_TRACK_PADDING) * CD_TRACK_PADDING;
}
Ok(())
}
fn extract_cd_toc<R: Read + Seek, P: FnMut(u32)>(
chd: &mut ChdFile<R>,
toc: &CdToc,
output_toc_path: &Path,
output_bin_path: Option<&Path>,
parent_dir: &Path,
toc_stem: &str,
mut on_frames: P,
) -> Result<(), DiscError> {
let default_bin = format!("{}.bin", toc_stem);
let bin_path_buf;
let (bin_path, bin_filename) = match output_bin_path {
Some(p) => (
p,
p.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&default_bin),
),
None => {
bin_path_buf = parent_dir.join(&default_bin);
(bin_path_buf.as_path(), default_bin.as_str())
}
};
let toc_content = CdrdaoToc::format(toc, bin_filename);
std::fs::write(output_toc_path, toc_content)?;
let bin_file = File::create(bin_path)?;
let mut writer = BufWriter::with_capacity(512 * 1024, bin_file);
let mut current_frame_idx: u32 = 0;
let mut frame_buf = vec![0u8; EXTRACT_FRAMES_BATCH * CD_TOTAL_FRAME_SIZE];
for track in &toc.tracks {
let sec_size = track.track_type.sector_size();
let data_frames = track.frames.saturating_sub(track.pad_frames);
extract_track_frames(
chd,
&mut writer,
&mut frame_buf,
current_frame_idx,
data_frames,
sec_size,
false,
&mut on_frames,
)?;
current_frame_idx += track.frames.div_ceil(CD_TRACK_PADDING) * CD_TRACK_PADDING;
}
writer.flush()?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn extract_cd_cue<R: Read + Seek, P: FnMut(u32)>(
chd: &mut ChdFile<R>,
toc: &CdToc,
output_toc_path: &Path,
output_bin_path: Option<&Path>,
parent_dir: &Path,
toc_stem: &str,
split_bin: bool,
mut on_frames: P,
) -> Result<(), DiscError> {
let mut current_frame_idx: u32 = 0;
let mut frame_buf = vec![0u8; EXTRACT_FRAMES_BATCH * CD_TOTAL_FRAME_SIZE];
if split_bin {
let cue_content = CueSheet::format_split_bin(toc, toc_stem);
std::fs::write(output_toc_path, cue_content)?;
for track in &toc.tracks {
let track_bin = format!("{} (Track {}).bin", toc_stem, track.track_num);
let file_path = parent_dir.join(track_bin);
let file = File::create(file_path)?;
let mut writer = BufWriter::with_capacity(512 * 1024, file);
let is_audio = track.track_type.is_audio();
let sec_size = track.track_type.sector_size();
let data_frames = track.frames.saturating_sub(track.pad_frames);
extract_track_frames(
chd,
&mut writer,
&mut frame_buf,
current_frame_idx,
data_frames,
sec_size,
is_audio,
&mut on_frames,
)?;
writer.flush()?;
current_frame_idx += track.frames.div_ceil(CD_TRACK_PADDING) * CD_TRACK_PADDING;
}
} else {
let default_bin = format!("{}.bin", toc_stem);
let bin_path_buf;
let (bin_path, bin_filename) = match output_bin_path {
Some(p) => (
p,
p.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&default_bin),
),
None => {
bin_path_buf = parent_dir.join(&default_bin);
(bin_path_buf.as_path(), default_bin.as_str())
}
};
let cue_content = CueSheet::format_single_bin(toc, bin_filename);
std::fs::write(output_toc_path, cue_content)?;
let bin_file = File::create(bin_path)?;
let mut writer = BufWriter::with_capacity(512 * 1024, bin_file);
for track in &toc.tracks {
let is_audio = track.track_type.is_audio();
let sec_size = track.track_type.sector_size();
let data_frames = track.frames.saturating_sub(track.pad_frames);
extract_track_frames(
chd,
&mut writer,
&mut frame_buf,
current_frame_idx,
data_frames,
sec_size,
is_audio,
&mut on_frames,
)?;
current_frame_idx += track.frames.div_ceil(CD_TRACK_PADDING) * CD_TRACK_PADDING;
}
writer.flush()?;
}
Ok(())
}
fn extract_cd_ccd<R: Read + Seek, P: FnMut(u32)>(
chd: &mut ChdFile<R>,
toc: &CdToc,
output_toc_path: &Path,
output_bin_path: Option<&Path>,
parent_dir: &Path,
toc_stem: &str,
mut on_frames: P,
) -> Result<(), DiscError> {
let has_raw_subcode = toc
.tracks
.iter()
.any(|t| t.subcode_type == CdSubcodeType::RwRaw);
if !has_raw_subcode {
return Err(DiscError::MissingSubcodeForCloneCd);
}
let default_img = format!("{}.img", toc_stem);
let img_path_buf;
let img_path = match output_bin_path {
Some(p) => p,
None => {
img_path_buf = parent_dir.join(&default_img);
img_path_buf.as_path()
}
};
let sub_path = img_path.with_extension("sub");
let ccd_content = CcdSheet::format(toc);
std::fs::write(output_toc_path, ccd_content)?;
let img_file = File::create(img_path)?;
let mut img_writer = BufWriter::with_capacity(512 * 1024, img_file);
let sub_file = File::create(&sub_path)?;
let mut sub_writer = BufWriter::with_capacity(128 * 1024, sub_file);
let mut current_frame_idx: u32 = 0;
let mut frame_buf = vec![0u8; EXTRACT_FRAMES_BATCH * CD_TOTAL_FRAME_SIZE];
for track in &toc.tracks {
let is_audio = track.track_type.is_audio();
let data_frames = track.frames.saturating_sub(track.pad_frames);
extract_clonecd_frames(
chd,
&mut img_writer,
&mut sub_writer,
&mut frame_buf,
current_frame_idx,
data_frames,
is_audio,
&mut on_frames,
)?;
current_frame_idx += track.frames.div_ceil(CD_TRACK_PADDING) * CD_TRACK_PADDING;
}
img_writer.flush()?;
sub_writer.flush()?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn extract_clonecd_frames<R: Read + Seek, W1: Write, W2: Write, P: FnMut(u32)>(
chd: &mut ChdFile<R>,
img_writer: &mut W1,
sub_writer: &mut W2,
frame_buf: &mut [u8],
start_frame_idx: u32,
data_frames: u32,
swap_endian: bool,
mut on_frames: P,
) -> Result<(), DiscError> {
let mut frames_remaining = data_frames;
let mut current_frame = start_frame_idx;
while frames_remaining > 0 {
let chunk_frames = (frames_remaining as usize).min(EXTRACT_FRAMES_BATCH);
let read_bytes_count = chunk_frames * CD_TOTAL_FRAME_SIZE;
let offset = current_frame as u64 * CD_TOTAL_FRAME_SIZE as u64;
let mut cursor = std::io::Cursor::new(&mut frame_buf[..read_bytes_count]);
chd.extract_range_parallel(offset, read_bytes_count as u64, &mut cursor)?;
for f in 0..chunk_frames {
let start = f * CD_TOTAL_FRAME_SIZE;
let sector_slice = &mut frame_buf[start..start + 2352];
if swap_endian {
swap_audio_endianness(sector_slice);
}
img_writer.write_all(sector_slice)?;
let sub_slice: &[u8; 96] =
frame_buf[start + 2352..start + 2448]
.try_into()
.map_err(|_| {
DiscError::InvalidMetadata("invalid subcode slice length".to_string())
})?;
let sub_frame = SubFrame::from_interleaved(sub_slice);
let mut sub_deint = [0u8; 96];
sub_frame.to_deinterleaved(&mut sub_deint);
sub_writer.write_all(&sub_deint)?;
}
on_frames(chunk_frames as u32);
current_frame += chunk_frames as u32;
frames_remaining -= chunk_frames as u32;
}
Ok(())
}
fn extract_cd_mds<R: Read + Seek, P: FnMut(u32)>(
chd: &mut ChdFile<R>,
toc: &CdToc,
output_toc_path: &Path,
output_bin_path: Option<&Path>,
parent_dir: &Path,
toc_stem: &str,
mut on_frames: P,
) -> Result<(), DiscError> {
let default_mdf = format!("{}.mdf", toc_stem);
let mdf_path_buf;
let mdf_path = match output_bin_path {
Some(p) => p,
None => {
mdf_path_buf = parent_dir.join(&default_mdf);
mdf_path_buf.as_path()
}
};
let mds_content = MdsDisc::format(toc);
std::fs::write(output_toc_path, mds_content)?;
let mdf_file = File::create(mdf_path)?;
let mut writer = BufWriter::with_capacity(512 * 1024, mdf_file);
let mut current_frame_idx: u32 = 0;
let mut frame_buf = vec![0u8; EXTRACT_FRAMES_BATCH * CD_TOTAL_FRAME_SIZE];
for track in &toc.tracks {
let is_audio = track.track_type.is_audio();
let data_frames = track.frames.saturating_sub(track.pad_frames);
let sec_size = if track.subcode_type == CdSubcodeType::RwRaw {
2448
} else {
track.track_type.sector_size()
};
extract_track_frames(
chd,
&mut writer,
&mut frame_buf,
current_frame_idx,
data_frames,
sec_size,
is_audio,
&mut on_frames,
)?;
current_frame_idx += track.frames.div_ceil(CD_TRACK_PADDING) * CD_TRACK_PADDING;
}
writer.flush()?;
Ok(())
}
fn extract_cd_nrg<R: Read + Seek, P: FnMut(u32)>(
chd: &mut ChdFile<R>,
toc: &CdToc,
output_nrg_path: &Path,
mut on_frames: P,
) -> Result<(), DiscError> {
let nrg_file = File::create(output_nrg_path)?;
let mut writer = BufWriter::with_capacity(512 * 1024, nrg_file);
let mut current_frame_idx: u32 = 0;
let mut current_payload_bytes: u64 = 0;
let mut frame_buf = vec![0u8; EXTRACT_FRAMES_BATCH * CD_TOTAL_FRAME_SIZE];
for track in &toc.tracks {
let is_audio = track.track_type.is_audio();
let data_frames = track.frames.saturating_sub(track.pad_frames);
let sec_size = if track.subcode_type == CdSubcodeType::RwRaw {
2448
} else {
track.track_type.sector_size()
};
if track.pregap_frames > 0 {
let sec_size_u64 = sec_size as u64;
let mut remaining = (track.pregap_frames as u64) * sec_size_u64;
static ZERO_BUF: [u8; 64 * 1024] = [0u8; 64 * 1024];
while remaining > 0 {
let to_write = (remaining.min(ZERO_BUF.len() as u64)) as usize;
writer.write_all(&ZERO_BUF[..to_write])?;
remaining -= to_write as u64;
}
current_payload_bytes += (track.pregap_frames as u64) * sec_size_u64;
}
extract_track_frames(
chd,
&mut writer,
&mut frame_buf,
current_frame_idx,
data_frames,
sec_size,
is_audio,
&mut on_frames,
)?;
current_payload_bytes += data_frames as u64 * sec_size as u64;
current_frame_idx += track.frames.div_ceil(CD_TRACK_PADDING) * CD_TRACK_PADDING;
}
let footer_bytes = NrgDisc::format_footer(toc, current_payload_bytes);
writer.write_all(&footer_bytes)?;
writer.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::create::{create_cd, CreateCdOptions};
use crate::types::{CdTrack, CdTrackType, DiscType};
fn make_synthetic_clonecd(dir: &Path) -> (std::path::PathBuf, Vec<u8>, Vec<u8>) {
let ccd_path = dir.join("disc.ccd");
let img_path = dir.join("disc.img");
let sub_path = dir.join("disc.sub");
let total_sectors = 8usize;
let mut img_data = vec![0u8; total_sectors * 2352];
let (chunks, _) = img_data.as_chunks_mut::<2352>();
for (i, chunk) in chunks.iter_mut().enumerate() {
chunk[0] = (i as u8).wrapping_mul(17) + 1;
chunk[2351] = (i as u8).wrapping_mul(23) + 2;
}
std::fs::write(&img_path, &img_data).unwrap();
let mut sub_data = vec![0u8; total_sectors * 96];
let (sub_chunks, _) = sub_data.as_chunks_mut::<96>();
for (i, chunk) in sub_chunks.iter_mut().enumerate() {
for (byte_idx, b) in chunk.iter_mut().enumerate() {
*b = ((i * 96 + byte_idx) as u8).wrapping_mul(7);
}
}
std::fs::write(&sub_path, &sub_data).unwrap();
let ccd_content = r#"[CloneCD]
Version=3
[Disc]
TocEntries=5
Tracks=2
CDTextLength=0
[Session 1]
PreGapMode=1
PreGapSubC=0
[Entry 0]
Session=1
Point=0xa0
ADR=0x01
Control=0x04
TrackNo=0
AMin=0
ASec=0
AFrame=0
ALBA=-150
Zero=0
PMin=1
PSec=0x20
PFrame=0
PLBA=0
[Entry 1]
Session=1
Point=0xa1
ADR=0x01
Control=0x04
TrackNo=0
AMin=0
ASec=0
AFrame=0
ALBA=-150
Zero=0
PMin=2
PSec=0x00
PFrame=0
PLBA=0
[Entry 2]
Session=1
Point=0xa2
ADR=0x01
Control=0x04
TrackNo=0
AMin=0
ASec=0
AFrame=0
ALBA=-150
Zero=0
PMin=0
PSec=0
PFrame=8
PLBA=8
[Entry 3]
Session=1
Point=0x01
ADR=0x01
Control=0x04
TrackNo=0
AMin=0
ASec=0
AFrame=0
ALBA=-150
Zero=0
PMin=0
PSec=2
PFrame=0
PLBA=0
[Entry 4]
Session=1
Point=0x02
ADR=0x01
Control=0x00
TrackNo=0
AMin=0
ASec=0
AFrame=0
ALBA=-150
Zero=0
PMin=0
PSec=2
PFrame=4
PLBA=4
[TRACK 1]
MODE=1
INDEX 1=0
[TRACK 2]
MODE=0
INDEX 1=4
"#;
std::fs::write(&ccd_path, ccd_content).unwrap();
(ccd_path, img_data, sub_data)
}
#[test]
fn test_extract_cd_clonecd_roundtrip_parity() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_extract_ccd_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let (ccd_path, original_img, original_sub) = make_synthetic_clonecd(&temp_dir);
let chd_path = temp_dir.join("disc.chd");
let create_opts = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
create_cd(&ccd_path, &chd_path, &create_opts, |_| {}).unwrap();
let out_dir = temp_dir.join("extracted");
let _ = std::fs::create_dir_all(&out_dir);
let out_ccd = out_dir.join("dump.ccd");
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let extracted_toc = extract_cd(&mut chd, &out_ccd, None, false).unwrap();
assert_eq!(extracted_toc.tracks.len(), 2);
assert!(out_ccd.exists());
let out_img = out_dir.join("dump.img");
let out_sub = out_dir.join("dump.sub");
assert!(out_img.exists());
assert!(out_sub.exists());
let extracted_img = std::fs::read(&out_img).unwrap();
let extracted_sub = std::fs::read(&out_sub).unwrap();
assert_eq!(
extracted_img, original_img,
"extracted .img payload does not match original byte-for-byte"
);
assert_eq!(
extracted_sub, original_sub,
"extracted .sub subchannels do not match original byte-for-byte"
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_extract_cd_clonecd_to_cue_graceful_degradation() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_ccd_to_cue_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let (ccd_path, original_img, _) = make_synthetic_clonecd(&temp_dir);
let chd_path = temp_dir.join("disc.chd");
let create_opts = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
create_cd(&ccd_path, &chd_path, &create_opts, |_| {}).unwrap();
let out_dir = temp_dir.join("extracted_cue");
let _ = std::fs::create_dir_all(&out_dir);
let out_cue = out_dir.join("dump.cue");
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let extracted_toc = extract_cd(&mut chd, &out_cue, None, false).unwrap();
assert_eq!(extracted_toc.tracks.len(), 2);
assert!(out_cue.exists());
let out_bin = out_dir.join("dump.bin");
assert!(out_bin.exists());
let extracted_bin = std::fs::read(&out_bin).unwrap();
assert_eq!(extracted_bin, original_img);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_extract_cd_cue_to_ccd_forbidden_flow() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_cue_to_ccd_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let cue_path = temp_dir.join("test.cue");
let bin_path = temp_dir.join("test.bin");
let chd_path = temp_dir.join("test.chd");
let bin_data = vec![0x33u8; 4 * 2352];
std::fs::write(&bin_path, &bin_data).unwrap();
let cue_content =
"FILE \"test.bin\" BINARY\n TRACK 01 MODE1/2352\n INDEX 01 00:00:00\n";
std::fs::write(&cue_path, cue_content).unwrap();
let create_opts = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
create_cd(&cue_path, &chd_path, &create_opts, |_| {}).unwrap();
let out_ccd = temp_dir.join("extracted.ccd");
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let res = extract_cd(&mut chd, &out_ccd, None, false);
match res {
Err(DiscError::MissingSubcodeForCloneCd) => {
let msg = DiscError::MissingSubcodeForCloneCd.to_string();
assert!(msg.contains("SUBTYPE: NONE"));
assert!(msg.contains(".cue"));
}
other => panic!("expected Err(MissingSubcodeForCloneCd), got {:?}", other),
}
assert!(!out_ccd.exists());
assert!(!temp_dir.join("extracted.img").exists());
assert!(!temp_dir.join("extracted.sub").exists());
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_extract_cd_mds_with_subcode_roundtrip_parity() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_extract_mds_sub_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let mds_path = temp_dir.join("disc.mds");
let mdf_path = temp_dir.join("disc.mdf");
let chd_path = temp_dir.join("disc.chd");
let total_sectors = 4usize;
let mut original_mdf = vec![0u8; total_sectors * 2448];
let (chunks, _) = original_mdf.as_chunks_mut::<2448>();
for (i, chunk) in chunks.iter_mut().enumerate() {
chunk[0] = (i as u8).wrapping_mul(17) + 1;
chunk[2351] = (i as u8).wrapping_mul(23) + 2;
for j in 0..96 {
chunk[2352 + j] = ((i * 96 + j) as u8).wrapping_mul(7);
}
}
std::fs::write(&mdf_path, &original_mdf).unwrap();
let mut toc = CdToc::new(DiscType::Cd);
let mut t1 = CdTrack::new(1, CdTrackType::Mode1Raw, total_sectors as u32);
t1.subcode_type = CdSubcodeType::RwRaw;
t1.file_name = Some("disc.mdf".to_string());
toc.tracks.push(t1);
let mds_bytes = MdsDisc::format(&toc);
std::fs::write(&mds_path, &mds_bytes).unwrap();
let create_opts = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
create_cd(&mds_path, &chd_path, &create_opts, |_| {}).unwrap();
let out_dir = temp_dir.join("extracted");
let _ = std::fs::create_dir_all(&out_dir);
let out_mds = out_dir.join("extracted.mds");
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let extracted_toc = extract_cd(&mut chd, &out_mds, None, false).unwrap();
assert_eq!(extracted_toc.tracks.len(), 1);
assert_eq!(extracted_toc.tracks[0].subcode_type, CdSubcodeType::RwRaw);
assert!(out_mds.is_file());
let out_mdf = out_dir.join("extracted.mdf");
assert!(out_mdf.is_file());
let extracted_mdf = std::fs::read(&out_mdf).unwrap();
assert_eq!(
extracted_mdf, original_mdf,
"extracted .mdf payload with subcode does not match original byte-for-byte"
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_extract_cd_mds_without_subcode_roundtrip_parity() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_extract_mds_nosub_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let mds_path = temp_dir.join("disc.mds");
let mdf_path = temp_dir.join("disc.mdf");
let chd_path = temp_dir.join("disc.chd");
let total_sectors = 4usize;
let mut original_mdf = vec![0u8; total_sectors * 2352];
let (chunks, _) = original_mdf.as_chunks_mut::<2352>();
for (i, chunk) in chunks.iter_mut().enumerate() {
chunk[0] = (i as u8).wrapping_mul(19) + 5;
chunk[2351] = (i as u8).wrapping_mul(29) + 6;
}
std::fs::write(&mdf_path, &original_mdf).unwrap();
let mut toc = CdToc::new(DiscType::Cd);
let mut t1 = CdTrack::new(1, CdTrackType::Mode1Raw, total_sectors as u32);
t1.subcode_type = CdSubcodeType::None;
t1.file_name = Some("disc.mdf".to_string());
toc.tracks.push(t1);
let mds_bytes = MdsDisc::format(&toc);
std::fs::write(&mds_path, &mds_bytes).unwrap();
let create_opts = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
create_cd(&mds_path, &chd_path, &create_opts, |_| {}).unwrap();
let out_dir = temp_dir.join("extracted");
let _ = std::fs::create_dir_all(&out_dir);
let out_mds = out_dir.join("extracted.mds");
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let extracted_toc = extract_cd(&mut chd, &out_mds, None, false).unwrap();
assert_eq!(extracted_toc.tracks.len(), 1);
assert_eq!(extracted_toc.tracks[0].subcode_type, CdSubcodeType::None);
assert!(out_mds.is_file());
let out_mdf = out_dir.join("extracted.mdf");
assert!(out_mdf.is_file());
let extracted_mdf = std::fs::read(&out_mdf).unwrap();
assert_eq!(
extracted_mdf, original_mdf,
"extracted .mdf payload without subcode does not match original byte-for-byte"
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_extract_cd_cue_to_mds_compatibility() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_cue_to_mds_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let cue_path = temp_dir.join("test.cue");
let bin_path = temp_dir.join("test.bin");
let chd_path = temp_dir.join("test.chd");
let bin_data = vec![0x77u8; 4 * 2352];
std::fs::write(&bin_path, &bin_data).unwrap();
let cue_content =
"FILE \"test.bin\" BINARY\n TRACK 01 MODE1/2352\n INDEX 01 00:00:00\n";
std::fs::write(&cue_path, cue_content).unwrap();
let create_opts = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
create_cd(&cue_path, &chd_path, &create_opts, |_| {}).unwrap();
let out_mds = temp_dir.join("extracted.mds");
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let res = extract_cd(&mut chd, &out_mds, None, false);
assert!(res.is_ok(), "extract_cd to mds failed: {:?}", res.err());
assert!(out_mds.is_file());
let out_mdf = temp_dir.join("extracted.mdf");
assert!(out_mdf.is_file());
let extracted_mdf = std::fs::read(&out_mdf).unwrap();
assert_eq!(extracted_mdf, bin_data);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_extract_cd_nrg_roundtrip_parity() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_extract_nrg_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let nrg_path = temp_dir.join("disc.nrg");
let chd_path = temp_dir.join("disc.chd");
let total_sectors = 4usize;
let mut original_payload = vec![0u8; total_sectors * 2352];
let (chunks, _) = original_payload.as_chunks_mut::<2352>();
for (i, chunk) in chunks.iter_mut().enumerate() {
chunk[0] = (i as u8).wrapping_mul(13) + 7;
chunk[2351] = (i as u8).wrapping_mul(17) + 9;
}
let mut toc = CdToc::new(DiscType::Cd);
let mut t1 = CdTrack::new(1, CdTrackType::Mode1Raw, total_sectors as u32);
t1.subcode_type = CdSubcodeType::None;
t1.file_name = Some("disc.nrg".to_string());
toc.tracks.push(t1);
let footer_bytes = NrgDisc::format_footer(&toc, original_payload.len() as u64);
let mut full_nrg = original_payload.clone();
full_nrg.extend_from_slice(&footer_bytes);
std::fs::write(&nrg_path, &full_nrg).unwrap();
let create_opts = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
create_cd(&nrg_path, &chd_path, &create_opts, |_| {}).unwrap();
let out_dir = temp_dir.join("extracted");
let _ = std::fs::create_dir_all(&out_dir);
let out_nrg = out_dir.join("extracted.nrg");
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let extracted_toc = extract_cd(&mut chd, &out_nrg, None, false).unwrap();
assert_eq!(extracted_toc.tracks.len(), 1);
assert_eq!(extracted_toc.tracks[0].subcode_type, CdSubcodeType::None);
assert!(out_nrg.is_file());
let extracted_bytes = std::fs::read(&out_nrg).unwrap();
assert_eq!(
&extracted_bytes[..total_sectors * 2352],
&original_payload[..],
"extracted NRG sector payload does not match original"
);
let parsed_nrg = NrgDisc::parse(&extracted_bytes).expect("parse extracted NRG");
assert_eq!(parsed_nrg.tracks.len(), 1);
assert_eq!(parsed_nrg.tracks[0].sector_size, 2352);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_extract_cd_cue_to_nrg_compatibility() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_cue_to_nrg_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let cue_path = temp_dir.join("test.cue");
let bin_path = temp_dir.join("test.bin");
let chd_path = temp_dir.join("test.chd");
let bin_data = vec![0x99u8; 4 * 2352];
std::fs::write(&bin_path, &bin_data).unwrap();
let cue_content =
"FILE \"test.bin\" BINARY\n TRACK 01 MODE1/2352\n INDEX 01 00:00:00\n";
std::fs::write(&cue_path, cue_content).unwrap();
let create_opts = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
create_cd(&cue_path, &chd_path, &create_opts, |_| {}).unwrap();
let out_nrg = temp_dir.join("extracted.nrg");
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let res = extract_cd(&mut chd, &out_nrg, None, false);
assert!(res.is_ok(), "extract_cd to nrg failed: {:?}", res.err());
assert!(out_nrg.is_file());
let extracted_bytes = std::fs::read(&out_nrg).unwrap();
assert_eq!(&extracted_bytes[..bin_data.len()], &bin_data[..]);
let _ = std::fs::remove_dir_all(&temp_dir);
}
}