use super::common::{find_wav_data_chunk, parse_cd_descriptor, DEFAULT_CD_CODECS};
use crate::error::DiscError;
use crate::metadata::create_metadata_from_toc;
use crate::sub::SubFrame;
use crate::subcode::swap_audio_endianness;
use crate::types::{CdSubcodeType, CdToc, CdTrack};
use chdlady_core::{
ChdFile, ChdWriteConfig, ChdWriter, CreateResult, MetadataItem, ProgressStatus,
};
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct CreateCdOptions {
pub hunk_size: Option<u32>,
pub codecs: Option<[u32; 4]>,
pub parent_path: Option<PathBuf>,
pub num_processors: Option<usize>,
pub cancel_token: Option<Arc<AtomicBool>>,
}
pub struct CdStreamReader {
parent_dir: PathBuf,
tracks: Vec<CdTrack>,
current_track: usize,
current_frame: u32,
open_file: Option<(PathBuf, BufReader<File>, u64)>,
current_file_pos: Option<u64>,
open_sub_file: Option<(PathBuf, BufReader<File>, u64)>,
current_sub_file_pos: Option<u64>,
frame_buf: [u8; 2448],
buf_pos: usize,
}
impl CdStreamReader {
pub fn new(parent_dir: PathBuf, toc: CdToc) -> Self {
Self {
parent_dir,
tracks: toc.tracks,
current_track: 0,
current_frame: 0,
open_file: None,
current_file_pos: None,
open_sub_file: None,
current_sub_file_pos: None,
frame_buf: [0u8; 2448],
buf_pos: 2448, }
}
fn read_next_frame(&mut self) -> io::Result<bool> {
if self.current_track >= self.tracks.len() {
return Ok(false);
}
let track = &self.tracks[self.current_track];
let total_frames = track.frames.next_multiple_of(4);
self.frame_buf.fill(0);
if self.current_frame < track.frames {
let is_gap_pad = track.pad_frames > 0
&& self.current_frame >= (track.frames.saturating_sub(track.pad_frames));
if !is_gap_pad {
let file_name = track.file_name.as_deref().unwrap_or_default();
let full_path = self.parent_dir.join(file_name);
let is_wav = full_path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("wav"));
let need_reopen = match &self.open_file {
Some((p, _, _)) => p != &full_path,
None => true,
};
if need_reopen {
let f = File::open(&full_path)?;
let reader = BufReader::with_capacity(512 * 1024, f);
let data_offset = if is_wav {
find_wav_data_chunk(&full_path)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
.0
} else {
0
};
self.open_file = Some((full_path, reader, data_offset));
self.current_file_pos = None;
}
if let Some((_, ref mut file, data_offset)) = self.open_file {
let sector_size = track.track_type.sector_size();
let frame_stride = if track.sub_file_name.is_none()
&& track.subcode_type != CdSubcodeType::None
{
sector_size + 96
} else {
sector_size
};
let file_pos = data_offset
+ track.file_offset
+ (self.current_frame as u64 * frame_stride as u64);
if self.current_file_pos != Some(file_pos) {
file.seek(SeekFrom::Start(file_pos))?;
}
file.read_exact(&mut self.frame_buf[..sector_size])?;
let mut next_pos = file_pos + sector_size as u64;
if track.sub_file_name.is_none() && track.subcode_type != CdSubcodeType::None {
let _ = file.read_exact(&mut self.frame_buf[2352..2448]);
next_pos += 96;
}
if track.track_type.is_audio() {
swap_audio_endianness(&mut self.frame_buf[..2352]);
}
self.current_file_pos = Some(next_pos);
}
if let Some(ref sub_name) = track.sub_file_name {
let sub_full_path = self.parent_dir.join(sub_name);
let need_sub_reopen = match &self.open_sub_file {
Some((p, _, _)) => p != &sub_full_path,
None => true,
};
if need_sub_reopen {
let f = File::open(&sub_full_path)?;
let reader = BufReader::with_capacity(128 * 1024, f);
self.open_sub_file = Some((sub_full_path, reader, 0));
self.current_sub_file_pos = None;
}
if let Some((_, ref mut sub_file, sub_data_offset)) = self.open_sub_file {
let sub_pos = sub_data_offset
+ track.sub_file_offset
+ (self.current_frame as u64 * 96);
if self.current_sub_file_pos != Some(sub_pos) {
sub_file.seek(SeekFrom::Start(sub_pos))?;
}
let mut sub_deint = [0u8; 96];
sub_file.read_exact(&mut sub_deint)?;
let sub_frame = SubFrame::from_deinterleaved(&sub_deint);
let interleaved = sub_frame.to_interleaved();
self.frame_buf[2352..2448].copy_from_slice(&interleaved);
self.current_sub_file_pos = Some(sub_pos + 96);
}
}
}
}
self.current_frame += 1;
if self.current_frame >= total_frames {
self.current_track += 1;
self.current_frame = 0;
}
self.buf_pos = 0;
Ok(true)
}
}
impl Read for CdStreamReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let mut written = 0;
while written < buf.len() {
if self.buf_pos < 2448 {
let available = 2448 - self.buf_pos;
let to_copy = available.min(buf.len() - written);
buf[written..written + to_copy]
.copy_from_slice(&self.frame_buf[self.buf_pos..self.buf_pos + to_copy]);
self.buf_pos += to_copy;
written += to_copy;
} else {
let has_more = self.read_next_frame()?;
if !has_more {
break;
}
}
}
Ok(written)
}
}
pub fn create_cd<F>(
input_path: &Path,
output_path: &Path,
options: &CreateCdOptions,
mut progress: F,
) -> Result<CreateResult, DiscError>
where
F: FnMut(ProgressStatus),
{
let parent_dir = input_path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
let toc = parse_cd_descriptor(input_path)?;
let mut totalsectors = 0u32;
for track in &toc.tracks {
let padded = track.frames.div_ceil(4);
let extra_frames = padded * 4 - track.frames;
totalsectors += track.frames + extra_frames;
}
let unit_size = 2448u32;
let hunk_size = options.hunk_size.unwrap_or(8 * 2448);
let logical_bytes = totalsectors as u64 * unit_size as u64;
let compressors = options.codecs.unwrap_or(DEFAULT_CD_CODECS);
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 config = ChdWriteConfig {
logical_bytes,
hunk_bytes: hunk_size,
unit_bytes: unit_size,
compressors,
parent_sha1,
num_processors: options.num_processors,
cancel_token: options.cancel_token.clone(),
};
let meta_entries = create_metadata_from_toc(&toc);
let metadata_items: Vec<MetadataItem> = meta_entries
.into_iter()
.map(|e| MetadataItem {
tag: e.metatag,
flags: e.flags,
value: e.value,
})
.collect();
let mut stream_reader = CdStreamReader::new(parent_dir, toc);
let output_file = File::create(output_path)?;
let mut writer = BufWriter::new(output_file);
let result = ChdWriter::write_chd_with_parent(
&mut stream_reader,
&mut writer,
&config,
&metadata_items,
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::*;
use crate::mds::MdsDisc;
use crate::{CdTrackType, DiscType};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
#[test]
fn test_create_cd_cancellation() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_cd_cancel_{}", 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 out_path = temp_dir.join("output.chd");
let bin_data = vec![0x77u8; 235200];
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 cancel_token = Arc::new(AtomicBool::new(true));
let options = CreateCdOptions {
cancel_token: Some(cancel_token),
..Default::default()
};
let result = create_cd(&cue_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);
}
#[test]
fn test_create_cd_from_clonecd() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_ccd_create_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let ccd_path = temp_dir.join("game.ccd");
let img_path = temp_dir.join("game.img");
let sub_path = temp_dir.join("game.sub");
let chd_path = temp_dir.join("game.chd");
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();
let options = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
let res = create_cd(&ccd_path, &chd_path, &options, |_| {});
assert!(res.is_ok(), "create_cd failed: {:?}", res.err());
assert!(chd_path.exists());
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
assert_eq!(chd.header().logical_bytes, (total_sectors * 2448) as u64);
let metadata = chd.read_all_metadata().unwrap();
assert_eq!(metadata.len(), 2);
let track1_meta = String::from_utf8_lossy(&metadata[0].value);
assert!(track1_meta.contains("TRACK:1"));
assert!(track1_meta.contains("TYPE:MODE1_RAW"));
assert!(track1_meta.contains("SUBTYPE:RW_RAW"));
assert!(track1_meta.contains("FRAMES:4"));
let track2_meta = String::from_utf8_lossy(&metadata[1].value);
assert!(track2_meta.contains("TRACK:2"));
assert!(track2_meta.contains("TYPE:AUDIO"));
assert!(track2_meta.contains("SUBTYPE:RW_RAW"));
assert!(track2_meta.contains("FRAMES:4"));
let mut frame0 = [0u8; 2448];
let mut cursor = std::io::Cursor::new(&mut frame0[..]);
chd.extract_range_parallel(0, 2448, &mut cursor).unwrap();
assert_eq!(frame0[0], img_data[0]);
assert_eq!(frame0[2351], img_data[2351]);
let sub_slice: &[u8; 96] = frame0[2352..2448].try_into().unwrap();
let frame_sub = SubFrame::from_interleaved(sub_slice);
let mut deint = [0u8; 96];
frame_sub.to_deinterleaved(&mut deint);
assert_eq!(&deint[..], &sub_data[0..96]);
if let Ok(output) = std::process::Command::new("chdman")
.args(["verify", "-i", chd_path.to_str().unwrap()])
.output()
{
assert!(
output.status.success(),
"chdman verify failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_create_cd_from_clonecd_without_sub() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_ccd_nosub_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let ccd_path = temp_dir.join("nosub.ccd");
let img_path = temp_dir.join("nosub.img");
let chd_path = temp_dir.join("nosub.chd");
let img_data = vec![0x42u8; 4 * 2352];
std::fs::write(&img_path, &img_data).unwrap();
let ccd_content = r#"[CloneCD]
Version=3
[Disc]
TocEntries=4
Tracks=1
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=1
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=4
PLBA=4
[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
[TRACK 1]
MODE=1
INDEX 1=0
"#;
std::fs::write(&ccd_path, ccd_content).unwrap();
let options = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
let res = create_cd(&ccd_path, &chd_path, &options, |_| {});
assert!(res.is_ok(), "create_cd failed: {:?}", res.err());
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let metadata = chd.read_all_metadata().unwrap();
let track_meta = String::from_utf8_lossy(&metadata[0].value);
assert!(track_meta.contains("SUBTYPE:NONE"));
let mut frame0 = [0u8; 2448];
let mut cursor = std::io::Cursor::new(&mut frame0[..]);
chd.extract_range_parallel(0, 2448, &mut cursor).unwrap();
assert_eq!(&frame0[0..2352], &img_data[0..2352]);
assert_eq!(&frame0[2352..2448], &[0u8; 96]);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_create_cd_from_mds_with_subcode() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_mds_create_{}", 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 mdf_data = vec![0u8; total_sectors * 2448];
let (chunks, _) = mdf_data.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, &mdf_data).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 options = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
let res = create_cd(&mds_path, &chd_path, &options, |_| {});
assert!(res.is_ok(), "create_cd failed: {:?}", res.err());
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let metadata = chd.read_all_metadata().unwrap();
let track1_meta = String::from_utf8_lossy(&metadata[0].value);
assert!(track1_meta.contains("TRACK:1"));
assert!(track1_meta.contains("TYPE:MODE1_RAW"));
assert!(track1_meta.contains("SUBTYPE:RW_RAW"));
assert!(track1_meta.contains("FRAMES:4"));
let mut extracted_all = vec![0u8; total_sectors * 2448];
let mut cursor = std::io::Cursor::new(&mut extracted_all[..]);
chd.extract_range_parallel(0, (total_sectors * 2448) as u64, &mut cursor)
.unwrap();
assert_eq!(&extracted_all[..], &mdf_data[..]);
if let Ok(output) = std::process::Command::new("chdman")
.args(["verify", "-i", chd_path.to_str().unwrap()])
.output()
{
assert!(
output.status.success(),
"chdman verify failed: stdout={}, stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_create_cd_from_mds_without_subcode() {
let temp_dir =
std::env::temp_dir().join(format!("chdlady_mds_nosub_{}", std::process::id()));
let _ = std::fs::create_dir_all(&temp_dir);
let mds_path = temp_dir.join("nosub.mds");
let mdf_path = temp_dir.join("nosub.mdf");
let chd_path = temp_dir.join("nosub.chd");
let total_sectors = 4usize;
let mut mdf_data = vec![0u8; total_sectors * 2352];
let (chunks, _) = mdf_data.as_chunks_mut::<2352>();
for (i, chunk) in chunks.iter_mut().enumerate() {
chunk[0] = (i as u8).wrapping_mul(13) + 3;
chunk[2351] = (i as u8).wrapping_mul(31) + 4;
}
std::fs::write(&mdf_path, &mdf_data).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("nosub.mdf".to_string());
toc.tracks.push(t1);
let mds_bytes = MdsDisc::format(&toc);
std::fs::write(&mds_path, &mds_bytes).unwrap();
let options = CreateCdOptions {
codecs: Some([0, 0, 0, 0]),
..Default::default()
};
let res = create_cd(&mds_path, &chd_path, &options, |_| {});
assert!(res.is_ok(), "create_cd failed: {:?}", res.err());
let f = File::open(&chd_path).unwrap();
let mut chd = ChdFile::open(f).unwrap();
let metadata = chd.read_all_metadata().unwrap();
let track1_meta = String::from_utf8_lossy(&metadata[0].value);
assert!(track1_meta.contains("TRACK:1"));
assert!(track1_meta.contains("TYPE:MODE1_RAW"));
assert!(track1_meta.contains("SUBTYPE:NONE"));
assert!(track1_meta.contains("FRAMES:4"));
let mut frame0 = [0u8; 2448];
let mut cursor = std::io::Cursor::new(&mut frame0[..]);
chd.extract_range_parallel(0, 2448, &mut cursor).unwrap();
assert_eq!(&frame0[0..2352], &mdf_data[0..2352]);
assert_eq!(&frame0[2352..2448], &[0u8; 96]);
let _ = std::fs::remove_dir_all(&temp_dir);
}
}