6use crate::cdtime::CdTime; 7use crate::cue::{CueSheet, Track, TrackMode, TrackType}; 8use crate::reader::{SECTOR_HEADER_LEN, synthesize_data_header}; 9use crate::{CdRomError, CdRomResult, cue}; 10use chd::Chd; 11use chd::iter::LendingIterator; 12use std::fmt::{Debug, Formatter}; 13use std::io::{Read, Seek}; 14 15#[derive(Debug, Clone, Copy)] 16#[cfg_attr(test, derive(PartialEq, Eq))] 17struct CdMetadata { 18 track_number: u8, 19 mode: TrackMode, 20 frames: u32, 21 pregap_frames: u32, 22} 23 24impl CdMetadata { 25 fn parse_from(ascii_bytes: Vec<u8>) -> Option<Self> { 26 let text = String::from_utf8(ascii_bytes).ok()?; 27 28 log::debug!("CHD metadata line: {text}"); 29 30 let mut track_number: Option<u8> = None; 31 let mut track_mode: Option<TrackMode> = None; 32 let mut frames: Option<u32> = None; 33 let mut pregap_frames: u32 = 0; 34 for token in text.split(' ') { 35 let Some((key, value)) = token.split_once(':') else { continue }; 36 37 match key { 38 "TRACK" => track_number = Some(value.parse().ok()?), 39 "TYPE" => match value { 40 "MODE1/2352" | "MODE1_RAW" => track_mode = Some(TrackMode::Mode1), 41 "MODE1/2048" | "MODE1" => track_mode = Some(TrackMode::Mode1DataOnly), 42 "MODE2/2352" | "MODE2_RAW" => track_mode = Some(TrackMode::Mode2), 43 "AUDIO" => track_mode = Some(TrackMode::Audio), 44 _ => return None, 45 }, 46 "FRAMES" => frames = Some(value.parse().ok()?), 47 "PREGAP" => pregap_frames = value.parse().ok()?, 48 _ => {} 49 } 50 } 51 52 Some(Self { 53 track_number: track_number?, 54 mode: track_mode?, 55 frames: frames?, 56 pregap_frames, 57 }) 58 } 59} 60 61pub struct ChdFile<F: Read + Seek> { 62 chd: Chd<F>, 63 cue: CueSheet, 64 track_start_frames: Vec<u32>, 65 compressed_buffer: Vec<u8>, 66 decompressed_buffer: Vec<u8>, 67 current_hunk_number: u32, 68} 69 70impl<F: Read + Seek> ChdFile<F> { 71 pub fn open(f: F) -> CdRomResult<(Self, CueSheet)> { 72 let mut chd = Chd::open(f, None)?; 73 74 // Parse TOC info from CHD metadata 75 let mut metadata_iter = chd.metadata(); 76 let mut cd_metadata_list = Vec::new(); 77 while let Some(mut metadata_entry) = metadata_iter.next() { 78 let metadata = metadata_entry.read()?; 79 80 let Some(cd_metadata) = CdMetadata::parse_from(metadata.value.clone()) else { 81 let value_str = String::from_utf8_lossy(&metadata.value).to_string(); 82 return Err(CdRomError::ChdHeaderParseError { metadata_value: value_str }); 83 }; 84 85 cd_metadata_list.push(cd_metadata); 86 } 87 88 cd_metadata_list.sort_by_key(|metadata| metadata.track_number); 89 validate_track_numbers(&cd_metadata_list)?; 90 91 // Use parsed info to build the TOC 92 let mut tracks = Vec::new(); 93 let mut track_start_frames = Vec::with_capacity(cd_metadata_list.len()); 94 let mut current_start_time = CdTime::ZERO; 95 let mut current_frame = 0; 96 for cd_metadata in cd_metadata_list { 97 let track_type = cd_metadata.mode.to_type(); 98 99 let pregap_len = match cd_metadata.track_number { 100 1 => { 101 // Track 1 always has a 2-second pregap; 00:02:00 should be first user data sector of track 1 102 CdTime::new(0, 2, 0) 103 } 104 _ => CdTime::ZERO, 105 }; 106 107 // CHD pregaps exist as data within the image, so count these frames as "pause" time 108 let pause_len = CdTime::from_frames(cd_metadata.pregap_frames); 109 110 let track_len = CdTime::from_frames(cd_metadata.frames); 111 let padded_track_len = pregap_len + track_len; 112 113 tracks.push(Track { 114 number: cd_metadata.track_number, 115 mode: cd_metadata.mode, 116 track_type, 117 start_time: current_start_time, 118 end_time: current_start_time + padded_track_len, 119 pregap_len, 120 pause_len, 121 postgap_len: CdTime::ZERO, 122 }); 123 track_start_frames.push(current_frame); 124 125 current_start_time += padded_track_len; 126 127 // CHD pads all tracks to a multiple of 4 frames 128 current_frame += cd_metadata.frames; 129 if current_frame % 4 != 0 { 130 current_frame = (current_frame & !3) + 4; 131 } 132 } 133 134 cue::finalize_track_list(&mut tracks); 135 136 assert!( 137 cue::tracks_are_continuous(&tracks), 138 "CHD tracks are not continuous; this is a bug" 139 ); 140 141 log::debug!("CHD track start frames: {track_start_frames:?}"); 142 143 let compressed_buffer = Vec::with_capacity(chd.header().hunk_size() as usize); 144 let decompressed_buffer = chd.get_hunksized_buffer(); 145 146 let cue_sheet = CueSheet::new(tracks); 147 let chd_file = Self { 148 chd, 149 cue: cue_sheet.clone(), 150 track_start_frames, 151 compressed_buffer, 152 decompressed_buffer, 153 current_hunk_number: u32::MAX, 154 }; 155 156 Ok((chd_file, cue_sheet)) 157 } 158 159 pub fn read_sector( 160 &mut self, 161 track_number: u8, 162 absolute_time: CdTime, 163 relative_sector_number: u32, 164 out: &mut [u8], 165 ) -> CdRomResult<()> { 166 let track_start_frame = self.track_start_frames[(track_number - 1) as usize]; 167 let sector_number = track_start_frame + relative_sector_number; 168 169 let hunk_size = self.chd.header().hunk_size(); 170 let unit_bytes = self.chd.header().unit_bytes(); 171 let hunk_size_sectors = hunk_size / unit_bytes; 172 173 let hunk_number = sector_number / hunk_size_sectors; 174 let hunk_offset_sectors = sector_number % hunk_size_sectors; 175 let hunk_offset_bytes = (hunk_offset_sectors * unit_bytes) as usize; 176 177 // Only load hunk if necessary 178 if hunk_number != self.current_hunk_number { 179 let mut hunk = self.chd.hunk(hunk_number)?; 180 181 self.compressed_buffer.clear(); 182 hunk.read_hunk_in(&mut self.compressed_buffer, &mut self.decompressed_buffer)?; 183 184 self.current_hunk_number = hunk_number; 185 } 186 187 let track = self.cue.track(track_number); 188 let track_mode = track.mode; 189 let track_type = track.track_type; 190 191 match track_mode { 192 TrackMode::Mode1DataOnly => { 193 // 2048-byte sectors 194 out[..SECTOR_HEADER_LEN as usize] 195 .copy_from_slice(&synthesize_data_header(track_mode, absolute_time)); 196 out[SECTOR_HEADER_LEN as usize..(SECTOR_HEADER_LEN + 2048) as usize] 197 .copy_from_slice( 198 &self.decompressed_buffer[hunk_offset_bytes..hunk_offset_bytes + 2048], 199 ); 200 out[(SECTOR_HEADER_LEN + 2048) as usize..crate::BYTES_PER_SECTOR as usize].fill(0); 201 } 202 TrackMode::Mode1 | TrackMode::Mode2 | TrackMode::Audio => { 203 out[..crate::BYTES_PER_SECTOR as usize].copy_from_slice( 204 &self.decompressed_buffer 205 [hunk_offset_bytes..hunk_offset_bytes + crate::BYTES_PER_SECTOR as usize], 206 ); 207 } 208 } 209 210 if track_type == TrackType::Audio { 211 // CHD audio tracks decompress into big-endian audio samples for some reason. Swap all 212 // the bytes to make them little-endian to match the CD-DA format 213 for chunk in out[..crate::BYTES_PER_SECTOR as usize].as_chunks_mut::<2>().0 { 214 chunk.swap(0, 1); 215 } 216 } 217 218 Ok(()) 219 } 220} 221 222impl<F: Read + Seek> Debug for ChdFile<F> { 223 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 224 write!(f, "ChdFile {{ current_hunk_number: {} }}", self.current_hunk_number) 225 } 226} 227 228fn validate_track_numbers(cd_metadata_list: &[CdMetadata]) -> CdRomResult<()> { 229 for (i, metadata) in cd_metadata_list.iter().enumerate() { 230 if metadata.track_number != (i + 1) as u8 { 231 let track_numbers = 232 cd_metadata_list.iter().map(|metadata| metadata.track_number).collect(); 233 return Err(CdRomError::ChdInvalidTrackList { track_numbers }); 234 } 235 } 236 237 Ok(()) 238}