cd_image_mds.cpp (9708B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR PolyForm-Strict-1.0.0) 3 4 #include "assert.h" 5 #include "cd_image.h" 6 #include "cd_subchannel_replacement.h" 7 8 #include "common/error.h" 9 #include "common/file_system.h" 10 #include "common/log.h" 11 #include "common/path.h" 12 13 #include <algorithm> 14 #include <cerrno> 15 #include <map> 16 17 Log_SetChannel(CDImageMds); 18 19 namespace { 20 21 #pragma pack(push, 1) 22 struct TrackEntry 23 { 24 u8 track_type; 25 u8 has_subchannel_data; 26 u8 unk1; 27 u8 unk2; 28 u8 track_number; 29 u8 unk3[4]; 30 u8 start_m; 31 u8 start_s; 32 u8 start_f; 33 u32 extra_offset; 34 u8 unk4[24]; 35 u32 track_offset_in_mdf; 36 u8 unk5[36]; 37 }; 38 static_assert(sizeof(TrackEntry) == 0x50, "TrackEntry is 0x50 bytes"); 39 #pragma pack(pop) 40 41 class CDImageMds : public CDImage 42 { 43 public: 44 CDImageMds(); 45 ~CDImageMds() override; 46 47 bool OpenAndParse(const char* filename, Error* error); 48 49 bool ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) override; 50 bool HasNonStandardSubchannel() const override; 51 s64 GetSizeOnDisk() const override; 52 53 protected: 54 bool ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) override; 55 56 private: 57 std::FILE* m_mdf_file = nullptr; 58 u64 m_mdf_file_position = 0; 59 CDSubChannelReplacement m_sbi; 60 }; 61 62 } // namespace 63 64 CDImageMds::CDImageMds() = default; 65 66 CDImageMds::~CDImageMds() 67 { 68 if (m_mdf_file) 69 std::fclose(m_mdf_file); 70 } 71 72 bool CDImageMds::OpenAndParse(const char* filename, Error* error) 73 { 74 std::FILE* mds_fp = FileSystem::OpenSharedCFile(filename, "rb", FileSystem::FileShareMode::DenyWrite, error); 75 if (!mds_fp) 76 { 77 Error::AddPrefixFmt(error, "Failed to open mds '{}': ", Path::GetFileName(filename)); 78 return false; 79 } 80 81 std::optional<DynamicHeapArray<u8>> mds_data_opt(FileSystem::ReadBinaryFile(mds_fp)); 82 std::fclose(mds_fp); 83 if (!mds_data_opt.has_value() || mds_data_opt->size() < 0x54) 84 { 85 ERROR_LOG("Failed to read mds file '{}'", Path::GetFileName(filename)); 86 Error::SetStringFmt(error, "Failed to read mds file '{}'", filename); 87 return false; 88 } 89 90 std::string mdf_filename(Path::ReplaceExtension(filename, "mdf")); 91 m_mdf_file = FileSystem::OpenSharedCFile(mdf_filename.c_str(), "rb", FileSystem::FileShareMode::DenyWrite, error); 92 if (!m_mdf_file) 93 { 94 Error::AddPrefixFmt(error, "Failed to open mdf file '{}': ", Path::GetFileName(mdf_filename)); 95 return false; 96 } 97 98 const DynamicHeapArray<u8>& mds = mds_data_opt.value(); 99 static constexpr char expected_signature[] = "MEDIA DESCRIPTOR"; 100 if (std::memcmp(&mds[0], expected_signature, sizeof(expected_signature) - 1) != 0) 101 { 102 ERROR_LOG("Incorrect signature in '{}'", Path::GetFileName(filename)); 103 Error::SetStringFmt(error, "Incorrect signature in '{}'", Path::GetFileName(filename)); 104 return false; 105 } 106 107 u32 session_offset; 108 std::memcpy(&session_offset, &mds[0x50], sizeof(session_offset)); 109 if ((session_offset + 24) > mds.size()) 110 { 111 ERROR_LOG("Invalid session offset in '{}'", Path::GetFileName(filename)); 112 Error::SetStringFmt(error, "Invalid session offset in '{}'", Path::GetFileName(filename)); 113 return false; 114 } 115 116 u16 track_count; 117 u32 track_offset; 118 std::memcpy(&track_count, &mds[session_offset + 14], sizeof(track_count)); 119 std::memcpy(&track_offset, &mds[session_offset + 20], sizeof(track_offset)); 120 if (track_count > 99 || track_offset >= mds.size()) 121 { 122 ERROR_LOG("Invalid track count/block offset {}/{} in '{}'", track_count, track_offset, Path::GetFileName(filename)); 123 Error::SetStringFmt(error, "Invalid track count/block offset {}/{} in '{}'", track_count, track_offset, 124 Path::GetFileName(filename)); 125 return false; 126 } 127 128 while ((track_offset + sizeof(TrackEntry)) <= mds.size()) 129 { 130 TrackEntry track; 131 std::memcpy(&track, &mds[track_offset], sizeof(track)); 132 if (track.track_number < 0xA0) 133 break; 134 135 track_offset += sizeof(TrackEntry); 136 } 137 138 for (u32 track_number = 1; track_number <= track_count; track_number++) 139 { 140 if ((track_offset + sizeof(TrackEntry)) > mds.size()) 141 { 142 ERROR_LOG("End of file in '{}' at track {}", Path::GetFileName(filename), track_number); 143 Error::SetStringFmt(error, "End of file in '{}' at track {}", Path::GetFileName(filename), track_number); 144 return false; 145 } 146 147 TrackEntry track; 148 std::memcpy(&track, &mds[track_offset], sizeof(track)); 149 track_offset += sizeof(TrackEntry); 150 151 if (PackedBCDToBinary(track.track_number) != track_number) 152 { 153 ERROR_LOG("Unexpected track number 0x{:02X} in track {}", track.track_number, track_number); 154 Error::SetStringFmt(error, "Unexpected track number 0x{:02X} in track {}", track.track_number, track_number); 155 return false; 156 } 157 158 const bool contains_subchannel = (track.has_subchannel_data != 0); 159 const u32 track_sector_size = (contains_subchannel ? 2448 : RAW_SECTOR_SIZE); 160 const TrackMode mode = (track.track_type == 0xA9) ? TrackMode::Audio : TrackMode::Mode2Raw; 161 162 if ((track.extra_offset + sizeof(u32) + sizeof(u32)) > mds.size()) 163 { 164 ERROR_LOG("Invalid extra offset {} in track {}", track.extra_offset, track_number); 165 Error::SetStringFmt(error, "Invalid extra offset {} in track {}", track.extra_offset, track_number); 166 return false; 167 } 168 169 u32 track_start_lba = Position::FromBCD(track.start_m, track.start_s, track.start_f).ToLBA(); 170 u32 track_file_offset = track.track_offset_in_mdf; 171 172 u32 track_pregap; 173 u32 track_length; 174 std::memcpy(&track_pregap, &mds[track.extra_offset], sizeof(track_pregap)); 175 std::memcpy(&track_length, &mds[track.extra_offset + sizeof(u32)], sizeof(track_length)); 176 177 // precompute subchannel q flags for the whole track 178 // todo: pull from mds? 179 SubChannelQ::Control control{}; 180 control.data = mode != TrackMode::Audio; 181 182 // create the index for the pregap 183 if (track_pregap > 0) 184 { 185 if (track_pregap > track_start_lba) 186 { 187 ERROR_LOG("Track pregap {} is too large for start lba {}", track_pregap, track_start_lba); 188 Error::SetStringFmt(error, "Track pregap {} is too large for start lba {}", track_pregap, track_start_lba); 189 return false; 190 } 191 192 Index pregap_index = {}; 193 pregap_index.start_lba_on_disc = track_start_lba - track_pregap; 194 pregap_index.start_lba_in_track = static_cast<LBA>(-static_cast<s32>(track_pregap)); 195 pregap_index.length = track_pregap; 196 pregap_index.track_number = track_number; 197 pregap_index.index_number = 0; 198 pregap_index.mode = mode; 199 pregap_index.submode = CDImage::SubchannelMode::None; 200 pregap_index.control.bits = control.bits; 201 pregap_index.is_pregap = true; 202 203 const bool pregap_in_file = (track_number > 1); 204 if (pregap_in_file) 205 { 206 pregap_index.file_index = 0; 207 pregap_index.file_offset = track_file_offset; 208 pregap_index.file_sector_size = track_sector_size; 209 track_file_offset += track_pregap * track_sector_size; 210 } 211 212 m_indices.push_back(pregap_index); 213 } 214 215 // add the track itself 216 m_tracks.push_back(Track{static_cast<u32>(track_number), track_start_lba, static_cast<u32>(m_indices.size()), 217 static_cast<u32>(track_length), mode, SubchannelMode::None, control}); 218 219 // how many indices in this track? 220 Index last_index; 221 last_index.start_lba_on_disc = track_start_lba; 222 last_index.start_lba_in_track = 0; 223 last_index.track_number = track_number; 224 last_index.index_number = 1; 225 last_index.file_index = 0; 226 last_index.file_sector_size = track_sector_size; 227 last_index.file_offset = track_file_offset; 228 last_index.mode = mode; 229 last_index.submode = CDImage::SubchannelMode::None; 230 last_index.control.bits = control.bits; 231 last_index.is_pregap = false; 232 last_index.length = track_length; 233 m_indices.push_back(last_index); 234 } 235 236 if (m_tracks.empty()) 237 { 238 ERROR_LOG("File '{}' contains no tracks", Path::GetFileName(filename)); 239 Error::SetStringFmt(error, "File '{}' contains no tracks", Path::GetFileName(filename)); 240 return false; 241 } 242 243 m_lba_count = m_tracks.back().start_lba + m_tracks.back().length; 244 AddLeadOutIndex(); 245 246 m_sbi.LoadFromImagePath(filename); 247 248 return Seek(1, Position{0, 0, 0}); 249 } 250 251 bool CDImageMds::ReadSubChannelQ(SubChannelQ* subq, const Index& index, LBA lba_in_index) 252 { 253 if (m_sbi.GetReplacementSubChannelQ(index.start_lba_on_disc + lba_in_index, subq)) 254 return true; 255 256 return CDImage::ReadSubChannelQ(subq, index, lba_in_index); 257 } 258 259 bool CDImageMds::HasNonStandardSubchannel() const 260 { 261 return (m_sbi.GetReplacementSectorCount() > 0); 262 } 263 264 bool CDImageMds::ReadSectorFromIndex(void* buffer, const Index& index, LBA lba_in_index) 265 { 266 const u64 file_position = index.file_offset + (static_cast<u64>(lba_in_index) * index.file_sector_size); 267 if (m_mdf_file_position != file_position) 268 { 269 if (std::fseek(m_mdf_file, static_cast<long>(file_position), SEEK_SET) != 0) 270 return false; 271 272 m_mdf_file_position = file_position; 273 } 274 275 // we don't want the subchannel data 276 const u32 read_size = RAW_SECTOR_SIZE; 277 if (std::fread(buffer, read_size, 1, m_mdf_file) != 1) 278 { 279 std::fseek(m_mdf_file, static_cast<long>(m_mdf_file_position), SEEK_SET); 280 return false; 281 } 282 283 m_mdf_file_position += read_size; 284 return true; 285 } 286 287 s64 CDImageMds::GetSizeOnDisk() const 288 { 289 return FileSystem::FSize64(m_mdf_file); 290 } 291 292 std::unique_ptr<CDImage> CDImage::OpenMdsImage(const char* filename, Error* error) 293 { 294 std::unique_ptr<CDImageMds> image = std::make_unique<CDImageMds>(); 295 if (!image->OpenAndParse(filename, error)) 296 return {}; 297 298 return image; 299 }