save_data_track/
save_data_track.rs1mod common;
19
20use std::fs::File;
21use std::io::{BufWriter, Write};
22use std::path::Path;
23
24use cd_da_reader::{CdReader, ReadOptions, SectorReadFormat, Toc};
25
26fn main() -> Result<(), Box<dyn std::error::Error>> {
27 let output_dir = common::fresh_output_dir("save_data_track")?;
28 let reader = CdReader::open_default()?;
29 let toc = reader.read_toc()?;
30
31 let data_track = toc
34 .tracks
35 .iter()
36 .find(|track| !track.is_audio)
37 .ok_or("no data track on this disc (need a mixed-mode / enhanced CD)")?;
38
39 let format = reader.detect_track_format(data_track)?;
40 println!("Data track #{} detected as {format:?}\n", data_track.number);
41
42 match format {
43 SectorReadFormat::Mode1Cooked => {
44 let iso_path = output_dir.join(format!("track{:02}.iso", data_track.number));
47 let bytes = stream_track_to_file(&reader, &toc, data_track.number, format, &iso_path)?;
48
49 println!(
50 "Wrote {} ({bytes} bytes, {} sectors)\n",
51 iso_path.display(),
52 bytes / format.sector_size() as u64
53 );
54 print_mount_hint(&iso_path.display().to_string());
55 }
56 SectorReadFormat::Mode2Raw => {
57 let bin_path = output_dir.join(format!("track{:02}.mode2.bin", data_track.number));
62 let bytes = stream_track_to_file(&reader, &toc, data_track.number, format, &bin_path)?;
63
64 println!(
65 "This is a Mode 2 track. Saved complete raw sectors to {} \
66 ({bytes} bytes, {} sectors).",
67 bin_path.display(),
68 bytes / format.sector_size() as u64
69 );
70 println!(
71 "Extracting a mountable filesystem from Mode 2 is consumer territory: \
72 each sector's XA subheader decides which bytes are user data."
73 );
74 }
75 other => {
76 return Err(format!(
77 "data track #{} detected as {other:?}, which is unexpected for a data track",
78 data_track.number
79 )
80 .into());
81 }
82 }
83
84 Ok(())
85}
86
87fn stream_track_to_file(
93 reader: &CdReader,
94 toc: &Toc,
95 track_no: u8,
96 format: SectorReadFormat,
97 path: &Path,
98) -> Result<u64, Box<dyn std::error::Error>> {
99 let options = ReadOptions::default().with_format(format);
100 let mut stream = reader.open_track_stream_with_options(toc, track_no, &options)?;
101
102 let total_sectors = stream.total_sectors();
103 let mut writer = BufWriter::new(File::create(path)?);
104 let mut written = 0u64;
105
106 while let Some(chunk) = stream.next_chunk()? {
107 writer.write_all(&chunk)?;
108 written += chunk.len() as u64;
109
110 let done = stream.current_sector();
111 let pct = done as f32 / total_sectors as f32 * 100.0;
112 eprint!("\r {done}/{total_sectors} sectors ({pct:5.1}%)");
113 }
114 eprintln!("\r {total_sectors}/{total_sectors} sectors (100.0%)");
115
116 writer.flush()?;
117 Ok(written)
118}
119
120fn print_mount_hint(path: &str) {
121 println!("Mount it and explore the files:");
122 if cfg!(target_os = "macos") {
123 println!(" hdiutil attach \"{path}\"");
124 } else if cfg!(target_os = "linux") {
125 println!(" sudo mount -o loop,ro \"{path}\" /mnt/cd");
126 } else if cfg!(target_os = "windows") {
127 println!(" PowerShell: Mount-DiskImage -ImagePath \"{path}\"");
128 }
129}