1//! Code for reading CD-ROM files
2
3mod chd;
4mod cuebin;
5
6use crate::cdtime::CdTime;
7use crate::cue::{CueSheet, TrackMode, TrackType};
8use crate::reader::chd::ChdFile;
9use crate::reader::cuebin::CdBinFiles;
10use crate::{CdRomError, CdRomResult};
11use bincode::{Decode, Encode};
12use jgenesis_proc_macros::{FakeDecode, FakeEncode};
13use std::ffi::OsStr;
14use std::fs;
15use std::fs::File;
16use std::io::{BufReader, Cursor};
17use std::path::Path;
18
19const SECTOR_HEADER_LEN: u64 = 16;
20
21type CdBinFsFiles = CdBinFiles<File>;
22type CdBinMemoryFiles = CdBinFiles<Cursor<Vec<u8>>>;
23
24type ChdFsFile = ChdFile<BufReader<File>>;
25type ChdMemoryFile = ChdFile<Cursor<Vec<u8>>>;
26
27#[derive(Debug, FakeEncode, FakeDecode)]
28enum CdRomReader {
29    CueBin(CdBinFsFiles),
30    CueBinMemory(CdBinMemoryFiles),
31    ChdFs(ChdFsFile),
32    ChdMemory(ChdMemoryFile),
33}
34
35impl Default for CdRomReader {
36    fn default() -> Self {
37        Self::CueBin(CdBinFiles::empty())
38    }
39}
40
41impl CdRomReader {
42    fn read_sector(
43        &mut self,
44        track_number: u8,
45        absolute_time: CdTime,
46        relative_sector_number: u32,
47        out: &mut [u8],
48    ) -> CdRomResult<()> {
49        match self {
50            Self::CueBin(bin_files) => {
51                bin_files.read_sector(track_number, absolute_time, relative_sector_number, out)
52            }
53            Self::CueBinMemory(bin_files) => {
54                bin_files.read_sector(track_number, absolute_time, relative_sector_number, out)
55            }
56            Self::ChdFs(chd_file) => {
57                chd_file.read_sector(track_number, absolute_time, relative_sector_number, out)
58            }
59            Self::ChdMemory(chd_file) => {
60                chd_file.read_sector(track_number, absolute_time, relative_sector_number, out)
61            }
62        }
63    }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum CdRomFileFormat {
68    // CUE file + BIN files
69    CueBin,
70    // CHD files
71    Chd,
72}
73
74impl CdRomFileFormat {
75    pub fn from_file_path<P: AsRef<Path>>(path: P) -> Option<Self> {
76        match path
77            .as_ref()
78            .extension()
79            .map(OsStr::to_ascii_lowercase)
80            .as_ref()
81            .and_then(|s| s.to_str())
82        {
83            Some("cue") => Some(Self::CueBin),
84            Some("chd") => Some(Self::Chd),
85            _ => None,
86        }
87    }
88}
89
90#[derive(Debug, Encode, Decode)]
91pub struct CdRom {
92    cue_sheet: CueSheet,
93    reader: CdRomReader,
94}
95
96impl CdRom {
97    /// Open a CD-ROM reader that will read from the filesystem as needed.
98    ///
99    /// # Errors
100    ///
101    /// Will propagate any I/O errors, and will return an error if the CD-ROM metadata appears
102    /// invalid.
103    pub fn open<P: AsRef<Path>>(path: P, format: CdRomFileFormat) -> CdRomResult<Self> {
104        match format {
105            CdRomFileFormat::CueBin => Self::open_cue_bin(path),
106            CdRomFileFormat::Chd => Self::open_chd(path),
107        }
108    }
109
110    fn open_cue_bin<P: AsRef<Path>>(cue_path: P) -> CdRomResult<Self> {
111        let (bin_files, cue_sheet) = CdBinFiles::create(cue_path, |path| File::open(path))?;
112
113        Ok(Self { cue_sheet, reader: CdRomReader::CueBin(bin_files) })
114    }
115
116    fn open_chd<P: AsRef<Path>>(chd_path: P) -> CdRomResult<Self> {
117        let chd_path = chd_path.as_ref();
118
119        let file = File::open(chd_path).map_err(|source| CdRomError::ChdOpen {
120            path: chd_path.display().to_string(),
121            source,
122        })?;
123        let (chd_file, cue_sheet) = ChdFile::open(BufReader::new(file))?;
124
125        Ok(Self { cue_sheet, reader: CdRomReader::ChdFs(chd_file) })
126    }
127
128    /// Open a CD-ROM reader that will load the entire disc image into memory.
129    ///
130    /// # Errors
131    ///
132    /// Will return any error encountered while reading from disk, or if the CD-ROM metadata appears
133    /// invalid.
134    pub fn open_in_memory<P: AsRef<Path>>(path: P, format: CdRomFileFormat) -> CdRomResult<Self> {
135        let path = path.as_ref();
136
137        match format {
138            CdRomFileFormat::CueBin => Self::open_cue_bin_in_memory(path),
139            CdRomFileFormat::Chd => {
140                let chd_bytes = fs::read(path).map_err(|source| CdRomError::ChdOpen {
141                    path: path.display().to_string(),
142                    source,
143                })?;
144                Self::open_chd_in_memory(chd_bytes)
145            }
146        }
147    }
148
149    /// Open a CD-ROM reader that will read from CUE/BIN files that will be read into memory.
150    ///
151    /// # Errors
152    ///
153    /// Will return any error encountered while reading from disk, or if the CUE file appears to be
154    /// invalid.
155    pub fn open_cue_bin_in_memory<P: AsRef<Path>>(cue_path: P) -> CdRomResult<Self> {
156        let (bin_files, cue_sheet) = CdBinFiles::create(cue_path, |path| {
157            let bin_bytes = fs::read(path)?;
158            Ok(Cursor::new(bin_bytes))
159        })?;
160
161        Ok(Self { reader: CdRomReader::CueBinMemory(bin_files), cue_sheet })
162    }
163
164    /// Open a CD-ROM reader that will read from a CHD file that has been read into memory.
165    ///
166    /// # Errors
167    ///
168    /// Will return an error if the CHD or CD-ROM metadata appears invalid.
169    pub fn open_chd_in_memory(chd_bytes: Vec<u8>) -> CdRomResult<Self> {
170        let (chd_file, cue_sheet) = ChdFile::open(Cursor::new(chd_bytes))?;
171
172        Ok(Self { cue_sheet, reader: CdRomReader::ChdMemory(chd_file) })
173    }
174
175    #[must_use]
176    pub fn cue(&self) -> &CueSheet {
177        &self.cue_sheet
178    }
179
180    /// Read a 2352-byte sector from the given track into a buffer.
181    ///
182    /// # Errors
183    ///
184    /// This method will propagate any I/O error encountered while reading from disk.
185    ///
186    /// # Panics
187    ///
188    /// This method will panic if `out`'s length is less than 2352 or if `relative_time` is past the
189    /// end of the track file.
190    pub fn read_sector(
191        &mut self,
192        track_number: u8,
193        relative_time: CdTime,
194        out: &mut [u8],
195    ) -> CdRomResult<()> {
196        let track = self.cue_sheet.track(track_number);
197        let absolute_time = relative_time + track.start_time;
198
199        if relative_time < track.pregap_len
200            || relative_time >= track.end_time - track.postgap_len - track.start_time
201        {
202            // Reading data in pregap or postgap that does not exist in the file
203            match track.track_type {
204                TrackType::Data => {
205                    write_fake_data_pregap(track.mode, absolute_time, out);
206                }
207                TrackType::Audio => {
208                    // Fill with all 0s
209                    out[..crate::BYTES_PER_SECTOR as usize].fill(0);
210                }
211            }
212            return Ok(());
213        }
214
215        let relative_sector_number = (relative_time - track.pregap_len).to_sector_number();
216        self.reader.read_sector(track_number, absolute_time, relative_sector_number, out)?;
217
218        Ok(())
219    }
220}
221
222impl TrackMode {
223    fn header_byte(self) -> u8 {
224        match self {
225            Self::Mode1 | Self::Mode1DataOnly => 0x01,
226            Self::Mode2 => 0x02,
227            Self::Audio => 0x00,
228        }
229    }
230}
231
232fn synthesize_data_header(mode: TrackMode, time: CdTime) -> [u8; 16] {
233    // Make up a header; 12 sync bytes, then MSF time, then mode byte
234    [
235        0x00,
236        0xFF,
237        0xFF,
238        0xFF,
239        0xFF,
240        0xFF,
241        0xFF,
242        0xFF,
243        0xFF,
244        0xFF,
245        0xFF,
246        0x00,
247        time_component_to_bcd(time.minutes),
248        time_component_to_bcd(time.seconds),
249        time_component_to_bcd(time.frames),
250        mode.header_byte(),
251    ]
252}
253
254fn write_fake_data_pregap(mode: TrackMode, time: CdTime, out: &mut [u8]) {
255    out[..SECTOR_HEADER_LEN as usize].copy_from_slice(&synthesize_data_header(mode, time));
256    out[SECTOR_HEADER_LEN as usize..crate::BYTES_PER_SECTOR as usize].fill(0);
257}
258
259fn time_component_to_bcd(component: u8) -> u8 {
260    let msb = component / 10;
261    let lsb = component % 10;
262    (msb << 4) | lsb
263}