iso_reader.cpp (11886B)
1 // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "iso_reader.h" 5 #include "cd_image.h" 6 7 #include "common/error.h" 8 #include "common/log.h" 9 #include "common/string_util.h" 10 11 #include "fmt/format.h" 12 13 #include <cctype> 14 15 Log_SetChannel(IsoReader); 16 17 IsoReader::IsoReader() = default; 18 19 IsoReader::~IsoReader() = default; 20 21 std::string_view IsoReader::RemoveVersionIdentifierFromPath(std::string_view path) 22 { 23 const std::string_view::size_type pos = path.find(';'); 24 return (pos != std::string_view::npos) ? path.substr(0, pos) : path; 25 } 26 27 bool IsoReader::Open(CDImage* image, u32 track_number, Error* error) 28 { 29 m_image = image; 30 m_track_number = track_number; 31 32 if (!ReadPVD(error)) 33 return false; 34 35 return true; 36 } 37 38 bool IsoReader::ReadSector(u8* buf, u32 lsn, Error* error) 39 { 40 if (!m_image->Seek(m_track_number, lsn)) 41 { 42 Error::SetString(error, fmt::format("Failed to seek to LSN #{}", lsn)); 43 return false; 44 } 45 46 if (m_image->Read(CDImage::ReadMode::DataOnly, 1, buf) != 1) 47 { 48 Error::SetString(error, fmt::format("Failed to read LSN #{}", lsn)); 49 return false; 50 } 51 52 return true; 53 } 54 55 bool IsoReader::ReadPVD(Error* error) 56 { 57 // volume descriptor start at sector 16 58 static constexpr u32 START_SECTOR = 16; 59 60 // try only a maximum of 256 volume descriptors 61 for (u32 i = 0; i < 256; i++) 62 { 63 u8 buffer[SECTOR_SIZE]; 64 if (!ReadSector(buffer, START_SECTOR + i, error)) 65 return false; 66 67 const ISOVolumeDescriptorHeader* header = reinterpret_cast<ISOVolumeDescriptorHeader*>(buffer); 68 if (std::memcmp(header->standard_identifier, "CD001", 5) != 0) 69 continue; 70 else if (header->type_code != 1) 71 continue; 72 else if (header->type_code == 255) 73 break; 74 75 m_pvd_lba = START_SECTOR + i; 76 std::memcpy(&m_pvd, buffer, sizeof(ISOPrimaryVolumeDescriptor)); 77 DEV_LOG("ISOReader: PVD found at index {}", i); 78 return true; 79 } 80 81 Error::SetString(error, "Failed to find the Primary Volume Descriptor."); 82 return false; 83 } 84 85 std::optional<IsoReader::ISODirectoryEntry> IsoReader::LocateFile(std::string_view path, Error* error) 86 { 87 const ISODirectoryEntry* root_de = reinterpret_cast<const ISODirectoryEntry*>(m_pvd.root_directory_entry); 88 if (path.empty() || path == "/" || path == "\\") 89 { 90 // locating the root directory 91 return *root_de; 92 } 93 94 // start at the root directory 95 u8 sector_buffer[SECTOR_SIZE]; 96 return LocateFile(path, sector_buffer, root_de->location_le, root_de->length_le, error); 97 } 98 99 std::string_view IsoReader::GetDirectoryEntryFileName(const u8* sector, u32 de_sector_offset) 100 { 101 const ISODirectoryEntry* de = reinterpret_cast<const ISODirectoryEntry*>(sector + de_sector_offset); 102 if ((sizeof(ISODirectoryEntry) + de->filename_length) > de->entry_length || 103 (sizeof(ISODirectoryEntry) + de->filename_length + de_sector_offset) > SECTOR_SIZE) 104 { 105 return std::string_view(); 106 } 107 108 const char* str = reinterpret_cast<const char*>(sector + de_sector_offset + sizeof(ISODirectoryEntry)); 109 if (de->filename_length == 1) 110 { 111 if (str[0] == '\0') 112 return "."; 113 else if (str[0] == '\1') 114 return ".."; 115 } 116 117 // Strip any version information like the PS2 BIOS does. 118 u32 length_without_version = 0; 119 for (; length_without_version < de->filename_length; length_without_version++) 120 { 121 if (str[length_without_version] == ';' || str[length_without_version] == '\0') 122 break; 123 } 124 125 return std::string_view(str, length_without_version); 126 } 127 128 std::optional<IsoReader::ISODirectoryEntry> IsoReader::LocateFile(std::string_view path, u8* sector_buffer, 129 u32 directory_record_lba, u32 directory_record_size, 130 Error* error) 131 { 132 if (directory_record_size == 0) 133 { 134 Error::SetString(error, fmt::format("Directory entry record size 0 while looking for '{}'", path)); 135 return std::nullopt; 136 } 137 138 // strip any leading slashes 139 size_t path_component_start = 0; 140 while (path_component_start < path.length() && 141 (path[path_component_start] == '/' || path[path_component_start] == '\\')) 142 { 143 path_component_start++; 144 } 145 146 size_t path_component_length = 0; 147 while ((path_component_start + path_component_length) < path.length() && 148 path[path_component_start + path_component_length] != '/' && 149 path[path_component_start + path_component_length] != '\\') 150 { 151 path_component_length++; 152 } 153 154 const std::string_view path_component = path.substr(path_component_start, path_component_length); 155 if (path_component.empty()) 156 { 157 Error::SetString(error, fmt::format("Empty path component in {}", path)); 158 return std::nullopt; 159 } 160 161 // start reading directory entries 162 const u32 num_sectors = (directory_record_size + (SECTOR_SIZE - 1)) / SECTOR_SIZE; 163 for (u32 i = 0; i < num_sectors; i++) 164 { 165 if (!ReadSector(sector_buffer, directory_record_lba + i, error)) 166 return std::nullopt; 167 168 u32 sector_offset = 0; 169 while ((sector_offset + sizeof(ISODirectoryEntry)) < SECTOR_SIZE) 170 { 171 const ISODirectoryEntry* de = reinterpret_cast<const ISODirectoryEntry*>(§or_buffer[sector_offset]); 172 if (de->entry_length < sizeof(ISODirectoryEntry)) 173 break; 174 175 const std::string_view de_filename = GetDirectoryEntryFileName(sector_buffer, sector_offset); 176 sector_offset += de->entry_length; 177 178 // Empty file would be pretty strange.. 179 if (de_filename.empty() || de_filename == "." || de_filename == "..") 180 continue; 181 182 if (de_filename.length() != path_component.length() || 183 StringUtil::Strncasecmp(de_filename.data(), path_component.data(), path_component.length()) != 0) 184 { 185 continue; 186 } 187 188 // found it. is this the file we're looking for? 189 if ((path_component_start + path_component_length) == path.length()) 190 return *de; 191 192 // if it is a directory, recurse into it 193 if (de->flags & ISODirectoryEntryFlag_Directory) 194 { 195 return LocateFile(path.substr(path_component_start + path_component_length), sector_buffer, de->location_le, 196 de->length_le, error); 197 } 198 199 // we're looking for a directory but got a file 200 Error::SetString(error, fmt::format("Looking for directory '{}' but got file", path_component)); 201 return std::nullopt; 202 } 203 } 204 205 Error::SetString(error, fmt::format("Path component '{}' not found", path_component)); 206 return std::nullopt; 207 } 208 209 std::vector<std::string> IsoReader::GetFilesInDirectory(std::string_view path, Error* error) 210 { 211 std::string base_path(path); 212 u32 directory_record_lsn; 213 u32 directory_record_length; 214 if (base_path.empty()) 215 { 216 // root directory 217 const ISODirectoryEntry* root_de = reinterpret_cast<const ISODirectoryEntry*>(m_pvd.root_directory_entry); 218 directory_record_lsn = root_de->location_le; 219 directory_record_length = root_de->length_le; 220 } 221 else 222 { 223 auto directory_de = LocateFile(base_path, error); 224 if (!directory_de.has_value()) 225 return {}; 226 227 if ((directory_de->flags & ISODirectoryEntryFlag_Directory) == 0) 228 { 229 Error::SetString(error, fmt::format("Path '{}' is not a directory, can't list", path)); 230 return {}; 231 } 232 233 directory_record_lsn = directory_de->location_le; 234 directory_record_length = directory_de->length_le; 235 236 if (base_path[base_path.size() - 1] != '/') 237 base_path += '/'; 238 } 239 240 // start reading directory entries 241 const u32 num_sectors = (directory_record_length + (SECTOR_SIZE - 1)) / SECTOR_SIZE; 242 std::vector<std::string> files; 243 u8 sector_buffer[SECTOR_SIZE]; 244 for (u32 i = 0; i < num_sectors; i++) 245 { 246 if (!ReadSector(sector_buffer, directory_record_lsn + i, error)) 247 break; 248 249 u32 sector_offset = 0; 250 while ((sector_offset + sizeof(ISODirectoryEntry)) < SECTOR_SIZE) 251 { 252 const ISODirectoryEntry* de = reinterpret_cast<const ISODirectoryEntry*>(§or_buffer[sector_offset]); 253 if (de->entry_length < sizeof(ISODirectoryEntry)) 254 break; 255 256 const std::string_view de_filename = GetDirectoryEntryFileName(sector_buffer, sector_offset); 257 sector_offset += de->entry_length; 258 259 // Empty file would be pretty strange.. 260 if (de_filename.empty() || de_filename == "." || de_filename == "..") 261 continue; 262 263 files.push_back(fmt::format("{}{}", base_path, de_filename)); 264 } 265 } 266 267 return files; 268 } 269 270 std::vector<std::pair<std::string, IsoReader::ISODirectoryEntry>> 271 IsoReader::GetEntriesInDirectory(std::string_view path, Error* error /*= nullptr*/) 272 { 273 std::string base_path(path); 274 u32 directory_record_lsn; 275 u32 directory_record_length; 276 if (base_path.empty()) 277 { 278 // root directory 279 const ISODirectoryEntry* root_de = reinterpret_cast<const ISODirectoryEntry*>(m_pvd.root_directory_entry); 280 directory_record_lsn = root_de->location_le; 281 directory_record_length = root_de->length_le; 282 } 283 else 284 { 285 auto directory_de = LocateFile(base_path, error); 286 if (!directory_de.has_value()) 287 return {}; 288 289 if ((directory_de->flags & ISODirectoryEntryFlag_Directory) == 0) 290 { 291 Error::SetString(error, fmt::format("Path '{}' is not a directory, can't list", path)); 292 return {}; 293 } 294 295 directory_record_lsn = directory_de->location_le; 296 directory_record_length = directory_de->length_le; 297 298 if (base_path[base_path.size() - 1] != '/') 299 base_path += '/'; 300 } 301 302 // start reading directory entries 303 const u32 num_sectors = (directory_record_length + (SECTOR_SIZE - 1)) / SECTOR_SIZE; 304 std::vector<std::pair<std::string, IsoReader::ISODirectoryEntry>> files; 305 u8 sector_buffer[SECTOR_SIZE]; 306 for (u32 i = 0; i < num_sectors; i++) 307 { 308 if (!ReadSector(sector_buffer, directory_record_lsn + i, error)) 309 break; 310 311 u32 sector_offset = 0; 312 while ((sector_offset + sizeof(ISODirectoryEntry)) < SECTOR_SIZE) 313 { 314 const ISODirectoryEntry* de = reinterpret_cast<const ISODirectoryEntry*>(§or_buffer[sector_offset]); 315 if (de->entry_length < sizeof(ISODirectoryEntry)) 316 break; 317 318 const std::string_view de_filename = GetDirectoryEntryFileName(sector_buffer, sector_offset); 319 sector_offset += de->entry_length; 320 321 // Empty file would be pretty strange.. 322 if (de_filename.empty() || de_filename == "." || de_filename == "..") 323 continue; 324 325 files.emplace_back(fmt::format("{}{}", base_path, de_filename), *de); 326 } 327 } 328 329 return files; 330 } 331 332 bool IsoReader::FileExists(std::string_view path, Error* error) 333 { 334 auto de = LocateFile(path, error); 335 if (!de) 336 return false; 337 338 return (de->flags & ISODirectoryEntryFlag_Directory) == 0; 339 } 340 341 bool IsoReader::DirectoryExists(std::string_view path, Error* error) 342 { 343 auto de = LocateFile(path, error); 344 if (!de) 345 return false; 346 347 return (de->flags & ISODirectoryEntryFlag_Directory) == ISODirectoryEntryFlag_Directory; 348 } 349 350 bool IsoReader::ReadFile(std::string_view path, std::vector<u8>* data, Error* error) 351 { 352 auto de = LocateFile(path, error); 353 if (!de) 354 return false; 355 356 return ReadFile(de.value(), data, error); 357 } 358 359 bool IsoReader::ReadFile(const ISODirectoryEntry& de, std::vector<u8>* data, Error* error /*= nullptr*/) 360 { 361 if (de.flags & ISODirectoryEntryFlag_Directory) 362 { 363 Error::SetString(error, "File is a directory"); 364 return false; 365 } 366 367 if (de.length_le == 0) 368 { 369 data->clear(); 370 return true; 371 } 372 373 const u32 num_sectors = (de.length_le + (SECTOR_SIZE - 1)) / SECTOR_SIZE; 374 data->resize(num_sectors * static_cast<size_t>(SECTOR_SIZE)); 375 for (u32 i = 0, lsn = de.location_le; i < num_sectors; i++, lsn++) 376 { 377 if (!ReadSector(data->data() + (i * SECTOR_SIZE), lsn, error)) 378 return false; 379 } 380 381 // Might not be sector aligned, so reduce it back. 382 data->resize(de.length_le); 383 return true; 384 }