Skip to main content

save_data_track/
save_data_track.rs

1//! Save the data track of a mixed-mode / enhanced ("CD-Extra") disc as a
2//! mountable image, then print how to mount it.
3//!
4//! This is the end-to-end data-track workflow:
5//!   1. read the TOC and pick the (first) data track,
6//!   2. auto-detect its sector format with `detect_track_format`,
7//!   3. for Mode 1, stream it **cooked** (2048 B/sector) straight to a `.iso` —
8//!      cooked Mode 1 is exactly the ISO 9660 filesystem image, so it mounts as
9//!      is. Streaming keeps memory flat regardless of track size (a full data
10//!      track can be hundreds of MB),
11//!   4. Mode 2 is detected but not auto-cooked here (see the note it prints).
12//!
13//! Reads and streams run over the same options and read path, so the only
14//! difference from a blocking `read_track_with_options` is that we pull chunks
15//! and write them as they arrive.
16//!
17//! Run with: `cargo run --example save_data_track`
18mod 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    // There is no `find_data_track` helper in the crate — the idiom is a plain
32    // filter on the TOC, since "data track" is simply `!is_audio`.
33    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            // Cooked Mode 1 strips sync/header/EDC/ECC, leaving exactly the
45            // 2048-byte user data per sector — i.e. the raw ISO 9660 image.
46            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            // Mode 2 forms are a per-sector property; producing a clean cooked
58            // payload requires inspecting each sector's XA subheader, which is
59            // left to the consumer. We save the complete raw sectors so nothing
60            // is lost.
61            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
87/// Stream one track straight to a file in `format`, without ever holding the
88/// whole track in memory. Returns the number of bytes written.
89///
90/// Uses the streaming API so peak memory is one chunk (~64 KB) instead of the
91/// entire track, which matters for large data images.
92fn 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}