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}