mod common;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use cd_da_reader::{CdReader, ReadOptions, SectorReadFormat, Toc};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let output_dir = common::fresh_output_dir("save_data_track")?;
let reader = CdReader::open_default()?;
let toc = reader.read_toc()?;
let data_track = toc
.tracks
.iter()
.find(|track| !track.is_audio)
.ok_or("no data track on this disc (need a mixed-mode / enhanced CD)")?;
let format = reader.detect_track_format(data_track)?;
println!("Data track #{} detected as {format:?}\n", data_track.number);
match format {
SectorReadFormat::Mode1Cooked => {
let iso_path = output_dir.join(format!("track{:02}.iso", data_track.number));
let bytes = stream_track_to_file(&reader, &toc, data_track.number, format, &iso_path)?;
println!(
"Wrote {} ({bytes} bytes, {} sectors)\n",
iso_path.display(),
bytes / format.sector_size() as u64
);
print_mount_hint(&iso_path.display().to_string());
}
SectorReadFormat::Mode2Raw => {
let bin_path = output_dir.join(format!("track{:02}.mode2.bin", data_track.number));
let bytes = stream_track_to_file(&reader, &toc, data_track.number, format, &bin_path)?;
println!(
"This is a Mode 2 track. Saved complete raw sectors to {} \
({bytes} bytes, {} sectors).",
bin_path.display(),
bytes / format.sector_size() as u64
);
println!(
"Extracting a mountable filesystem from Mode 2 is consumer territory: \
each sector's XA subheader decides which bytes are user data."
);
}
other => {
return Err(format!(
"data track #{} detected as {other:?}, which is unexpected for a data track",
data_track.number
)
.into());
}
}
Ok(())
}
fn stream_track_to_file(
reader: &CdReader,
toc: &Toc,
track_no: u8,
format: SectorReadFormat,
path: &Path,
) -> Result<u64, Box<dyn std::error::Error>> {
let options = ReadOptions::default().with_format(format);
let mut stream = reader.open_track_stream_with_options(toc, track_no, &options)?;
let total_sectors = stream.total_sectors();
let mut writer = BufWriter::new(File::create(path)?);
let mut written = 0u64;
while let Some(chunk) = stream.next_chunk()? {
writer.write_all(&chunk)?;
written += chunk.len() as u64;
let done = stream.current_sector();
let pct = done as f32 / total_sectors as f32 * 100.0;
eprint!("\r {done}/{total_sectors} sectors ({pct:5.1}%)");
}
eprintln!("\r {total_sectors}/{total_sectors} sectors (100.0%)");
writer.flush()?;
Ok(written)
}
fn print_mount_hint(path: &str) {
println!("Mount it and explore the files:");
if cfg!(target_os = "macos") {
println!(" hdiutil attach \"{path}\"");
} else if cfg!(target_os = "linux") {
println!(" sudo mount -o loop,ro \"{path}\" /mnt/cd");
} else if cfg!(target_os = "windows") {
println!(" PowerShell: Mount-DiskImage -ImagePath \"{path}\"");
}
}