From b212935c702d228b29bbacf11f6fe02e86b655d8 Mon Sep 17 00:00:00 2001 From: Antonino Di Guardo <64427768+digant73@users.noreply.github.com> Date: Wed, 29 Apr 2026 04:42:49 +0200 Subject: [PATCH] Add support to play from a BD Drive (currently only on Windows) (#18648) Follow up of #18345 to add support (currently only on `Windows`; see notes below) for playing a PS3 disc game directly from a Blu-Ray Disc Drive. ### HOW IT WORKS: - The BD drive can be added as any other game so from `VFS games` or from `Add Games` menu. In case it is selected from `VFS games`, any attempt to write files is discarded, e.g. file `Disc Games Can Be Put Here For Automatic Detection.txt` - It scans the default redump keys folder `/data/redump` (it currently needs to be manually created due it is not yet provided by rpcs3 installation) to find a matching decryption key ### NOTES: - Support is currently provided on `Windows` where I can fully test it. I cannot test under other OS. However, the additions needed for the other OS are limited only on `fs::file.h/cpp`. In particular inside the following new functions: - `bool is_optical_raw_device(const std::string& path);` - `bool get_optical_raw_device(const std::string& path, std::string* raw_device = nullptr);` - Icons etc. are always refreshed (ISO cache cannot be used due `mtime` on raw device is not available and any cache check would always fail) - Code in `ISO.h/cpp` needed some rework to properly manage a read on a raw device (alignment on offset, size and memory is mandatory). The BD drive needs to be detected as a file, not as a folder ### MINOR FIXES: - Fixed wrong specifier used in logging on `ISO.h/cpp` --- Utilities/File.cpp | 106 ++++- Utilities/File.h | 6 + rpcs3/Emu/System.cpp | 22 +- rpcs3/Loader/ISO.cpp | 626 +++++++++++++++++++++++------- rpcs3/Loader/ISO.h | 41 +- rpcs3/rpcs3qt/game_list_frame.cpp | 31 +- rpcs3/rpcs3qt/game_list_grid.cpp | 2 +- rpcs3/rpcs3qt/game_list_table.cpp | 8 +- rpcs3/rpcs3qt/main_window.cpp | 2 +- rpcs3/rpcs3qt/qt_utils.cpp | 16 +- rpcs3/rpcs3qt/qt_video_source.cpp | 12 +- rpcs3/rpcs3qt/shortcut_utils.cpp | 8 +- 12 files changed, 668 insertions(+), 212 deletions(-) diff --git a/Utilities/File.cpp b/Utilities/File.cpp index aff4537dea..d58331e0e2 100644 --- a/Utilities/File.cpp +++ b/Utilities/File.cpp @@ -38,6 +38,17 @@ static std::unique_ptr to_wchar(std::string_view source) // Buffer for max possible output length std::unique_ptr buffer(new wchar_t[buf_size + 8 + 32768]); + // If path points to an optical raw device, copy it AS IS + if (fs::is_optical_raw_device(std::string(source))) + { + ensure(MultiByteToWideChar(CP_UTF8, 0, source.data(), size, buffer.get() + 32768, size)); // "to_wchar" + + // Canonicalize wide path (replace '/', ".", "..", \\ repetitions, etc) + ensure(GetFullPathNameW(buffer.get() + 32768, 32768, buffer.get(), nullptr) - 1 < 32768 - 1); // "to_wchar" + + return buffer; + } + // Prepend wide path prefix (4 characters) std::memcpy(buffer.get() + 32768, L"\\\\\?\\", 4 * sizeof(wchar_t)); @@ -400,11 +411,12 @@ namespace fs class windows_file final : public file_base { HANDLE m_handle; + bool m_raw_device; atomic_t m_pos {0}; public: - windows_file(HANDLE handle) - : m_handle(handle) + windows_file(HANDLE handle, bool raw_device = false) + : m_handle(handle), m_raw_device(raw_device) { } @@ -564,11 +576,20 @@ namespace fs u64 size() override { - // NOTE: this can fail if we access a mounted empty drive (e.g. after unmounting an iso). - LARGE_INTEGER size; - ensure(GetFileSizeEx(m_handle, &size)); // "file::size" + if (!m_raw_device) + { + // NOTE: this can fail if we access a mounted empty drive (e.g. after unmounting an iso). + LARGE_INTEGER size; - return size.QuadPart; + ensure(GetFileSizeEx(m_handle, &size)); // "file::size" + return size.QuadPart; + } + + // For a raw device, we need to use DeviceIoControl. + DISK_GEOMETRY_EX geometry; + + ensure(DeviceIoControl(m_handle, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, nullptr, 0, &geometry, sizeof(geometry), nullptr, nullptr)); + return geometry.DiskSize.QuadPart; } native_handle get_handle() override @@ -1091,6 +1112,68 @@ bool fs::is_symlink(const std::string& path) return true; } +bool fs::is_optical_raw_device(const std::string& path) +{ +#ifdef _WIN32 + if (path.starts_with("\\\\.\\")) + { + return true; + } + + return false; +#endif + return false; +} + +bool fs::get_optical_raw_device(const std::string& path, std::string* raw_device) +{ + if (fs::is_optical_raw_device(path)) + { + if (raw_device) + { + *raw_device = path; + } + + return true; + } + +#ifdef _WIN32 + constexpr u32 BUF_SIZE = 1000; + WCHAR drive_list[BUF_SIZE] = {0}; + + // GetLogicalDriveStrings() returns a double-null terminated list of null-terminated strings. + // E.g. A:\B:\C:\ + const DWORD copied = GetLogicalDriveStrings(BUF_SIZE, drive_list); + + if (copied == 0 || copied > BUF_SIZE) + { + return false; + } + + for (const WCHAR* drive = drive_list; drive && *drive; drive += wcslen(drive) + 1) + { + if (GetDriveType(drive) == DRIVE_CDROM) + { + const std::wstring ws(drive); + const std::string s = std::string(ws.begin(), ws.end() - 1); + + if (path.starts_with(s)) + { + if (raw_device) + { + *raw_device = "\\\\.\\" + s; + } + + return true; + } + } + } + + return false; +#endif + return false; +} + bool fs::statfs(const std::string& path, fs::device_stat& info) { if (auto device = get_virtual_device(path)) @@ -1658,9 +1741,18 @@ fs::file::file(const std::string& path, bs_t mode) return; } + // If path points to an optical raw device, complete the file opening + // (the following GetFileInformationByHandle() would always fail on a raw device). + if (is_optical_raw_device(path)) + { + m_file = std::make_unique(handle, true); + return; + } + // Check if the handle is actually valid. // This can fail on empty mounted drives (e.g. with ERROR_NOT_READY or ERROR_INVALID_FUNCTION). BY_HANDLE_FILE_INFORMATION info{}; + if (!GetFileInformationByHandle(handle, &info)) { const DWORD last_error = GetLastError(); @@ -1671,7 +1763,7 @@ fs::file::file(const std::string& path, bs_t mode) g_tls_error = fs::error::isdir; return; } - + g_tls_error = to_error(last_error); return; } diff --git a/Utilities/File.h b/Utilities/File.h index 3d332dd0be..778bfacd48 100644 --- a/Utilities/File.h +++ b/Utilities/File.h @@ -213,6 +213,12 @@ namespace fs // Check whether the path points to an existing symlink bool is_symlink(const std::string& path); + // Check whether the path points to a raw device + bool is_optical_raw_device(const std::string& path); + + // Check whether the path points to an optical drive. If so, provide the raw device in "raw_device" if requested + bool get_optical_raw_device(const std::string& path, std::string* raw_device = nullptr); + // Get filesystem information bool statfs(const std::string& path, device_stat& info); diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index 492aed799a..7f0437648b 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -638,7 +638,7 @@ void Emulator::Init() const std::string games_common_dir = g_cfg_vfs.get(g_cfg_vfs.games_dir, emu_dir); - if (make_path_verbose(games_common_dir, true)) + if (!is_iso_file(games_common_dir) && make_path_verbose(games_common_dir, true)) { fs::write_file(games_common_dir + "/Disc Games Can Be Put Here For Automatic Detection.txt", fs::create + fs::excl + fs::write, ""s); @@ -985,7 +985,7 @@ game_boot_result Emulator::BootGame(const std::string& path, const std::string& m_db_config = db_config; // Handle files and special paths inside Load unmodified - if (direct || !fs::is_dir(path)) + if (direct || !fs::is_dir(path) || fs::get_optical_raw_device(path)) { m_path = path; @@ -1201,7 +1201,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, std::string disc_info; m_ar->serialize(argv.emplace_back(), disc_info, klic.emplace_back(), m_game_dir, hdd1); - launching_from_disc_archive = is_file_iso(disc_info); + launching_from_disc_archive = is_iso_file(disc_info); sys_log.notice("Savestate: is iso archive = %d ('%s')", launching_from_disc_archive, disc_info); @@ -1218,7 +1218,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, // Load /dev_bdvd/ from game list if available if (std::string game_path = m_games_config.get_path(m_title_id); !game_path.empty()) { - if (is_file_iso(game_path)) + if (is_iso_file(game_path)) { game_path = iso_device::virtual_device_name + "/PS3_GAME/./"; } @@ -1389,7 +1389,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, title_path = std::move(game_path); } - if (is_file_iso(title_path)) + if (is_iso_file(title_path)) { m_path = std::move(title_path); ok = true; @@ -1480,7 +1480,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, } const std::string resolved_path = GetCallbacks().resolve_path(m_path); - if (!launching_from_disc_archive && is_file_iso(m_path)) + if (!launching_from_disc_archive && is_iso_file(m_path)) { sys_log.notice("Loading iso archive '%s'", m_path); @@ -1933,7 +1933,7 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch, // Load /dev_bdvd/ from game list if available if (std::string game_path = m_games_config.get_path(m_title_id); !game_path.empty()) { - if (is_file_iso(game_path)) + if (is_iso_file(game_path)) { sys_log.notice("Loading iso archive for patch ('%s')", game_path); @@ -4237,7 +4237,7 @@ u32 Emulator::AddGamesFromDir(const std::string& path) // search direct subdirectories, that way we can drop one folder containing all games for (; path_it != entries.end(); ++path_it) { - auto dir_entry = std::move(*path_it); + const auto dir_entry = std::move(*path_it); if (dir_entry.name == "." || dir_entry.name == "..") { @@ -4246,7 +4246,7 @@ u32 Emulator::AddGamesFromDir(const std::string& path) const std::string dir_path = path + '/' + dir_entry.name; - if (!dir_entry.is_directory && !is_file_iso(dir_path)) + if (!dir_entry.is_directory && !is_iso_file(dir_path)) { continue; } @@ -4283,7 +4283,7 @@ u32 Emulator::AddGamesFromDir(const std::string& path) game_boot_result Emulator::AddGame(const std::string& path) { // Handle files directly - if (!fs::is_dir(path)) + if (!fs::is_dir(path) || fs::get_optical_raw_device(path)) { return AddGameToYml(path); } @@ -4354,7 +4354,7 @@ game_boot_result Emulator::AddGameToYml(const std::string& path) } std::unique_ptr archive; - if (is_file_iso(path)) + if (is_iso_file(path)) { archive = std::make_unique(path); } diff --git a/rpcs3/Loader/ISO.cpp b/rpcs3/Loader/ISO.cpp index e79373fcb4..f5ba9f839c 100644 --- a/rpcs3/Loader/ISO.cpp +++ b/rpcs3/Loader/ISO.cpp @@ -10,6 +10,7 @@ #include #include #include +#include LOG_CHANNEL(sys_log, "SYS"); LOG_CHANNEL(iso_log, "ISO"); @@ -22,32 +23,83 @@ struct iso_sector u64 address_aligned; u64 offset_aligned; u64 size_aligned; - std::array buf; }; -bool is_file_iso(const std::string& path) +static void* get_aligned_buf() { - if (path.empty() || fs::is_dir(path)) + static thread_local struct aligned_buf { - return false; - } + void* buf; - return is_file_iso(fs::file(path)); + aligned_buf() noexcept + { + // IMPORTANT NOTE: It must be aligned (probably enough on multiple of 4) to support raw device, otherwise any read from file will fail +#if defined(_WIN32) + buf = _aligned_malloc(ISO_SECTOR_SIZE, ISO_SECTOR_SIZE * 2); +#else + buf = std::aligned_alloc(ISO_SECTOR_SIZE * 2, ISO_SECTOR_SIZE); +#endif + } + + ~aligned_buf() noexcept + { +#if defined(_WIN32) + _aligned_free(buf); +#else + std::free(buf); +#endif + } + } s_aligned_buf {}; + + return s_aligned_buf.buf; } -bool is_file_iso(const fs::file& file) +static bool is_iso_file(const fs::file& file, u64* size = nullptr) { - if (!file || file.size() < 32768 + 6) + if (!file || file.size() < 32768ULL + 6) { return false; } - file.seek(32768); - char magic[5]; - file.read_at(32768 + 1, magic, 5); - return magic[0] == 'C' && magic[1] == 'D' && magic[2] == '0' && magic[3] == '0' && magic[4] == '1'; + file.read_at(32768ULL + 1, magic, 5); + + const bool ret = magic[0] == 'C' && magic[1] == 'D' && magic[2] == '0' && magic[3] == '0' && magic[4] == '1'; + + if (size && ret) + { + *size = file.size(); + } + + return ret; +} + +bool is_iso_file(const std::string& path, u64* size, bool* is_raw_device) +{ + if (path.empty()) + { + return false; + } + + std::string new_path = path; + + // "new_path" is updated with the raw device path in case "path" points to a BD drive + const bool raw_device = fs::get_optical_raw_device(path, &new_path); + + if (!raw_device && fs::is_dir(path)) + { + return false; + } + + if (is_raw_device) + { + *is_raw_device = raw_device; + } + + fs::file file(std::make_unique(new_path, fs::read)); + + return is_iso_file(file, size); } // Convert 4 bytes in big-endian format to an unsigned integer @@ -68,7 +120,7 @@ static void reset_iv(std::array& iv, u32 lba) } // Main function that will decrypt the sector(s) -static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u64 size) +static bool decrypt_data(aes_context& aes, u64 offset, const unsigned char* buffer, unsigned char* out_buffer, u64 size) { // The following preliminary checks are good to be provided. // Commented out to gain a bit of performance, just because we know the caller is providing values in the expected range @@ -80,7 +132,7 @@ static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u6 //if ((size % 16) != 0) //{ - // iso_log.error("decrypt_data: Requested ciphertext blocks' size must be a multiple of 16 (%ull)", size); + // iso_log.error("decrypt_data: Requested ciphertext blocks' size must be a multiple of 16 (%llu)", size); // return; //} @@ -97,7 +149,7 @@ static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u6 // Otherwise, the IV is based on sector's LBA if (sector_offset != 0) { - memcpy(iv.data(), buffer, 16); + std::memcpy(iv.data(), buffer, 16); cur_offset = 16; } else @@ -110,7 +162,7 @@ static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u6 cur_size -= cur_offset; // Partial (or even full) first sector - if (aes_crypt_cbc(&aes, AES_DECRYPT, cur_size, iv.data(), &buffer[cur_offset], &buffer[cur_offset]) != 0) + if (aes_crypt_cbc(&aes, AES_DECRYPT, cur_size, iv.data(), &buffer[cur_offset], &out_buffer[cur_offset]) != 0) { iso_log.error("decrypt_data: Error decrypting data on first sector read"); return false; @@ -130,7 +182,7 @@ static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u6 { reset_iv(iv, ++cur_sector_lba); // Next sector's IV - if (aes_crypt_cbc(&aes, AES_DECRYPT, ISO_SECTOR_SIZE, iv.data(), &buffer[cur_offset], &buffer[cur_offset]) != 0) + if (aes_crypt_cbc(&aes, AES_DECRYPT, ISO_SECTOR_SIZE, iv.data(), &buffer[cur_offset], &out_buffer[cur_offset]) != 0) { iso_log.error("decrypt_data: Error decrypting data on inner sector(s) read"); return false; @@ -142,7 +194,7 @@ static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u6 reset_iv(iv, ++cur_sector_lba); // Next sector's IV // Partial (or even full) last sector - if (aes_crypt_cbc(&aes, AES_DECRYPT, size - cur_offset, iv.data(), &buffer[cur_offset], &buffer[cur_offset]) != 0) + if (aes_crypt_cbc(&aes, AES_DECRYPT, size - cur_offset, iv.data(), &buffer[cur_offset], &out_buffer[cur_offset]) != 0) { iso_log.error("decrypt_data: Error decrypting data on last sector read"); return false; @@ -151,45 +203,9 @@ static bool decrypt_data(aes_context& aes, u64 offset, unsigned char* buffer, u6 return true; } -void iso_file_decryption::reset() +iso_type_status iso_file_decryption::get_key(const std::string& key_path, aes_context* aes_ctx) { - m_enc_type = iso_encryption_type::NONE; - m_region_info.clear(); -} - -iso_type_status iso_file_decryption::check_type(const std::string& path, std::string& key_path, aes_context* aes_ctx) -{ - if (!is_file_iso(path)) - { - return iso_type_status::NOT_ISO; - } - - // Remove file extension from file path - const usz ext_pos = path.rfind('.'); - const std::string name_path = ext_pos == umax ? path : path.substr(0, ext_pos); - - // Detect file name (with no parent folder and no file extension) - const usz name_pos = name_path.rfind('/'); - const std::string name = name_pos == umax ? name_path : name_path.substr(name_pos); - fs::file key_file; - - const std::array key_paths { - name_path + ".dkey", - name_path + ".key", - rpcs3::utils::get_redump_key_dir() + name + ".dkey", - rpcs3::utils::get_redump_key_dir() + name + ".key" - }; - - for (const std::string& path : key_paths) - { - key_file = fs::file(path); - - if (key_file) - { - key_path = path; - break; - } - } + fs::file key_file(key_path); // If no ".dkey" and ".key" file exists if (!key_file) @@ -208,7 +224,7 @@ iso_type_status iso_file_decryption::check_type(const std::string& path, std::st // binary (".key") and so not needing any further conversion from hex string to bytes if (key_len == sizeof(key)) { - memcpy(key.data(), key_str, sizeof(key)); + std::memcpy(key.data(), key_str, sizeof(key)); } else { @@ -234,32 +250,152 @@ iso_type_status iso_file_decryption::check_type(const std::string& path, std::st return iso_type_status::ERROR_PROCESSING_KEY; } -bool iso_file_decryption::init(const std::string& path) +iso_type_status iso_file_decryption::retrieve_key(iso_archive& archive, std::string& key_path, aes_context& aes_ctx) { - reset(); + // + // Find the first existing file in the archive present on the list of well known encrypted files to use for testing a matching key + // - if (!is_file_iso(path)) + const std::map dec_magics { + {"PS3_GAME/LICDIR/LIC.DAT", "PS3LICDA"}, + {"PS3_GAME/USRDIR/EBOOT.BIN", "SCE"} + }; + + iso_fs_node* node = nullptr; + std::string magic_value; + + for (const auto& magic : dec_magics) { - return false; + if (node = archive.retrieve(magic.first)) + { + magic_value = magic.second; + break; + } } + if (!node) + { + return iso_type_status::ERROR_OPENING_KEY; + } + + // + // Read the first encrypted sector to use for testing a matching key + // + + std::array enc_sec; + std::array dec_sec; + iso_file iso_file(archive.path(), fs::read, *node); + + if (!iso_file || iso_file.read(enc_sec.data(), ISO_SECTOR_SIZE) != ISO_SECTOR_SIZE) + { + return iso_type_status::NOT_ISO; + } + + // + // Scan all the key files present in the redump keys folder, decrypt the read sector and test for a match with file's magic value + // + + std::vector entries; + + for (auto&& dir_entry : fs::dir(rpcs3::utils::get_redump_key_dir())) + { + // Prefetch entries, it is unsafe to keep fs::dir for a long time or for many operations + entries.emplace_back(std::move(dir_entry)); + } + + for (auto path_it = entries.begin(); path_it != entries.end(); path_it++) + { + const auto dir_entry = std::move(*path_it); + + if (dir_entry.name == "." || dir_entry.name == ".." || dir_entry.is_directory) + { + continue; + } + + key_path = rpcs3::utils::get_redump_key_dir() + dir_entry.name; + + // If no valid key is present on the file + if (get_key(key_path, &aes_ctx) != iso_type_status::REDUMP_ISO) + { + continue; + } + + // If the decryption fails + if (!decrypt_data(aes_ctx, iso_file.file_offset(0), enc_sec.data(), dec_sec.data(), ISO_SECTOR_SIZE)) + { + continue; + } + + // If the decrypted data match the magic value + if (std::memcmp(magic_value.data(), dec_sec.data(), magic_value.size()) == 0) + { + return iso_type_status::REDUMP_ISO; + } + } + + return iso_type_status::ERROR_OPENING_KEY; +} + +iso_type_status iso_file_decryption::check_type(const std::string& path, std::string& key_path, aes_context* aes_ctx) +{ + if (!is_iso_file(path)) + { + return iso_type_status::NOT_ISO; + } + + // Remove file extension from file path + const usz ext_pos = path.rfind('.'); + const std::string name_path = ext_pos == umax ? path : path.substr(0, ext_pos); + + // Detect file name (with no parent folder and no file extension) + const usz name_pos = name_path.rfind('/'); + const std::string name = name_pos == umax ? name_path : name_path.substr(name_pos); + + const std::array key_paths { + name_path + ".dkey", + name_path + ".key", + rpcs3::utils::get_redump_key_dir() + name + ".dkey", + rpcs3::utils::get_redump_key_dir() + name + ".key" + }; + + for (const std::string& path : key_paths) + { + if (fs::is_file(path)) + { + key_path = path; + return get_key(key_path, aes_ctx); + } + } + + return iso_type_status::ERROR_OPENING_KEY; +} + +bool iso_file_decryption::init(const std::string& path, iso_archive* archive) +{ + // Reset attributes first + m_enc_type = iso_encryption_type::NONE; + m_region_info.clear(); + // // Store the ISO region information (needed by both the "Redump" type (only on "decrypt()" method) and "3k3y" type) // - fs::file iso_file(path); + fs::file iso_file(std::make_unique(path, fs::read)); - if (!iso_file) + if (!is_iso_file(iso_file)) { - iso_log.error("init: Failed to open file: %s", path); + iso_log.error("init: Failed to recognize ISO file: %s", path); return false; } - std::array sec0_sec1 {}; + // Reset the file position after it was changed by is_iso_file() + iso_file.seek(0); + + std::array sec0_sec1; if (iso_file.size() < sec0_sec1.size()) { - iso_log.error("init: Found only %ull sector(s) (minimum required is 2): %s", iso_file.size(), path); + iso_log.error("init: Found only %llu sector(s) (minimum required is 2): %s", iso_file.size(), path); return false; } @@ -279,13 +415,13 @@ bool iso_file_decryption::init(const std::string& path) // Ensure the region count is a proper value if (region_count < 1 || region_count > 127) // It's non-PS3ISO { - iso_log.error("init: Failed to read region information: '%s' (region_count=%d)", path, region_count); + iso_log.error("init: Failed to read region information (region_count=%lu): %s", region_count, path); return false; } m_region_info.resize(region_count * 2 - 1); - for (size_t i = 0; i < m_region_info.size(); ++i) + for (size_t i = 0; i < m_region_info.size(); i++) { // Store the region information in address format m_region_info[i].encrypted = (i % 2 == 1); @@ -298,15 +434,28 @@ bool iso_file_decryption::init(const std::string& path) // Check for Redump type // + iso_type_status status; std::string key_path; - // Try to detect the Redump type. If so, the decryption context is set into "m_aes_dec" - switch (check_type(path, key_path, &m_aes_dec)) + // If raw device and requested by the caller ("archive" provided), scan the redump keys folder and retrieve + // (if present) the first key that allows decrypting a sector of the ISO file + if (fs::is_optical_raw_device(path) && archive) + { + status = retrieve_key(*archive, key_path, m_aes_dec); + } + else + { + // Try to detect the Redump type. If so, the decryption context is set into "m_aes_dec" + status = check_type(path, key_path, &m_aes_dec); + } + + switch (status) { case iso_type_status::NOT_ISO: iso_log.warning("init: Failed to recognize ISO file: %s", path); break; case iso_type_status::REDUMP_ISO: + iso_log.warning("init: Found matching key file: %s", key_path); m_enc_type = iso_encryption_type::REDUMP; // SET ENCRYPTION TYPE: REDUMP break; case iso_type_status::ERROR_OPENING_KEY: @@ -332,12 +481,12 @@ bool iso_file_decryption::init(const std::string& path) static const unsigned char k3k3y_dec_watermark[16] = {0x44, 0x6E, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x20, 0x33, 0x4B, 0x20, 0x42, 0x4C, 0x44}; - if (memcmp(&k3k3y_enc_watermark[0], &sec0_sec1[0xF70], sizeof(k3k3y_enc_watermark)) == 0) + if (std::memcmp(&k3k3y_enc_watermark[0], &sec0_sec1[0xF70], sizeof(k3k3y_enc_watermark)) == 0) { // Grab D1 from the 3k3y sector unsigned char key[16]; - memcpy(key, &sec0_sec1[0xF80], 0x10); + std::memcpy(key, &sec0_sec1[0xF80], 0x10); // Convert D1 to KEY and generate the "m_aes_dec" context unsigned char key_d1[] = {0x38, 11, 0xcf, 11, 0x53, 0x45, 0x5b, 60, 120, 0x17, 0xab, 0x4f, 0xa3, 0xba, 0x90, 0xed}; @@ -361,7 +510,7 @@ bool iso_file_decryption::init(const std::string& path) iso_log.error("init: Failed to set encryption type to ENC_3K3Y: %s", path); } } - else if (memcmp(&k3k3y_dec_watermark[0], &sec0_sec1[0xF70], sizeof(k3k3y_dec_watermark)) == 0) + else if (std::memcmp(&k3k3y_dec_watermark[0], &sec0_sec1[0xF70], sizeof(k3k3y_dec_watermark)) == 0) { m_enc_type = iso_encryption_type::DEC_3K3Y; // SET ENCRYPTION TYPE: DEC_3K3Y } @@ -425,7 +574,7 @@ bool iso_file_decryption::decrypt(u64 offset, void* buffer, u64 size, const std: } // Decrypt the region before sending it back - decrypt_data(m_aes_dec, offset, reinterpret_cast(buffer), size); + decrypt_data(m_aes_dec, offset, reinterpret_cast(buffer), reinterpret_cast(buffer), size); return true; } @@ -441,6 +590,159 @@ bool iso_file_decryption::decrypt(u64 offset, void* buffer, u64 size, const std: return true; } +iso_file_encrypted::iso_file_encrypted(const std::string& path, bs_t mode, const iso_fs_node& node, std::shared_ptr dec) + : iso_file(path, mode, node), m_dec(dec) +{ +} + +u64 iso_file_encrypted::read_at(u64 offset, void* buffer, u64 size) +{ + // IMPORTANT NOTES: + // - For a raw device, we must use a support buffer aligned (probably enough on multiple of 4), otherwise any read from file will fail. + // For that reason, we don't use directly "buffer" (not guaranteeing any alignment) + // - "iso_file_decryption::decrypt()" method requires that offset and size are multiple of 16 bytes (ciphertext block's size) + // and that a previous ciphertext block (used as IV) is read in case offset is not a multiple of ISO_SECTOR_SIZE + // + // ---------------------------------------------------------------------- + // file on ISO archive: | ' ' | + // ---------------------------------------------------------------------- + // ' ' + // --------------------------------------------- + // buffer: | | + // --------------------------------------------- + // ' ' ' ' + // ------------------------------------------------------------------------------------------------------------------------------------- + // ISO archive: | sec 0 | sec 1 |xxxxx######'###########'###########'###########'##xxxxxxxxx| | ... | sec n-1 | sec n | + // ------------------------------------------------------------------------------------------------------------------------------------- + // 16 Bytes x block read: | | | | | | | '#######'###########'###########'###########'###| | | | | | | | | | | | | | | + // ' ' ' ' + // | first sec | inner sec(s) | last sec | + + const u64 max_size = std::min(size, local_extent_remaining(offset)); + + if (max_size == 0) + { + return 0; + } + + const u64 archive_first_offset = file_offset(offset); + const u64 archive_last_offset = archive_first_offset + max_size - 1; + iso_sector first_sec, last_sec; + void* aligned_buf = get_aligned_buf(); // thread-safe buffer + + first_sec.lba_address = (archive_first_offset / ISO_SECTOR_SIZE) * ISO_SECTOR_SIZE; + first_sec.offset = archive_first_offset % ISO_SECTOR_SIZE; + first_sec.size = first_sec.offset + max_size <= ISO_SECTOR_SIZE ? max_size : ISO_SECTOR_SIZE - first_sec.offset; + + last_sec.lba_address = last_sec.address_aligned = (archive_last_offset / ISO_SECTOR_SIZE) * ISO_SECTOR_SIZE; + // last_sec.offset = last_sec.offset_aligned = 0; // Always 0 so no need to set and use those attributes + last_sec.size = (archive_last_offset % ISO_SECTOR_SIZE) + 1; + + // + // First sector + // + + u64 offset_aligned_first_out = 0; + + if (!m_raw_device) + { + const u64 offset_aligned = first_sec.offset & ~0xF; + offset_aligned_first_out = (first_sec.offset + first_sec.size) & ~0xF; + + first_sec.offset_aligned = offset_aligned != 0 ? offset_aligned - 16 : 0; // Eventually include the previous block (used as IV) + first_sec.size_aligned = offset_aligned_first_out != (first_sec.offset + first_sec.size) ? + offset_aligned_first_out + 16 - first_sec.offset_aligned : + offset_aligned_first_out - first_sec.offset_aligned; + first_sec.address_aligned = first_sec.lba_address + first_sec.offset_aligned; + } + else + { + first_sec.offset_aligned = 0; + first_sec.size_aligned = ISO_SECTOR_SIZE; + first_sec.address_aligned = first_sec.lba_address; + } + + u64 total_read = m_file.read_at(first_sec.address_aligned, &reinterpret_cast(aligned_buf)[first_sec.offset_aligned], first_sec.size_aligned); + + m_dec->decrypt(first_sec.address_aligned, &reinterpret_cast(aligned_buf)[first_sec.offset_aligned], first_sec.size_aligned, m_meta.name); + std::memcpy(buffer, &reinterpret_cast(aligned_buf)[first_sec.offset], first_sec.size); + + const u64 sector_count = (last_sec.lba_address - first_sec.lba_address) / ISO_SECTOR_SIZE + 1; + + if (sector_count < 2) // If no more sector(s) + { + if (total_read != first_sec.size_aligned) + { + iso_log.error("read_at: %s: Error reading from file (%llu/%llu)", m_meta.name, total_read, first_sec.size_aligned); + return 0; + } + + return max_size; + } + + // + // Inner sector(s), if any + // + + if (sector_count > 2) // If inner sector(s) are present + { + if (!m_raw_device) + { + const u64 inner_sector_size = (sector_count - 2) * ISO_SECTOR_SIZE; + + total_read += m_file.read_at(first_sec.lba_address + ISO_SECTOR_SIZE, &reinterpret_cast(buffer)[first_sec.size], inner_sector_size); + + m_dec->decrypt(first_sec.lba_address + ISO_SECTOR_SIZE, &reinterpret_cast(buffer)[first_sec.size], inner_sector_size, m_meta.name); + } + else + { + u64 inner_sector_offset = 0; + + for (u64 i = 0; i < sector_count - 2; i++, inner_sector_offset += ISO_SECTOR_SIZE) + { + total_read += m_file.read_at(first_sec.lba_address + ISO_SECTOR_SIZE + inner_sector_offset, aligned_buf, ISO_SECTOR_SIZE); + + m_dec->decrypt(first_sec.lba_address + ISO_SECTOR_SIZE + inner_sector_offset, aligned_buf, ISO_SECTOR_SIZE, m_meta.name); + std::memcpy(&reinterpret_cast(buffer)[first_sec.size + inner_sector_offset], aligned_buf, ISO_SECTOR_SIZE); + } + } + } + + // + // Last sector + // + + if (!m_raw_device) + { + offset_aligned_first_out = last_sec.size & ~0xF; + + last_sec.size_aligned = offset_aligned_first_out != last_sec.size ? offset_aligned_first_out + 16 : offset_aligned_first_out; + } + else + { + last_sec.size_aligned = ISO_SECTOR_SIZE; + } + + total_read += m_file.read_at(last_sec.address_aligned, aligned_buf, last_sec.size_aligned); + + m_dec->decrypt(last_sec.address_aligned, aligned_buf, last_sec.size_aligned, m_meta.name); + std::memcpy(&reinterpret_cast(buffer)[max_size - last_sec.size], aligned_buf, last_sec.size); + + // + // As last, check for an unlikely reading error (decoding also failed due to use of partially initialized buffer) + // + + if (total_read != first_sec.size_aligned + last_sec.size_aligned + (sector_count - 2) * ISO_SECTOR_SIZE) + { + iso_log.error("read_at: %s: Error reading from file (%llu/%llu)", m_meta.name, + total_read, ISO_SECTOR_SIZE + ISO_SECTOR_SIZE + (sector_count - 2) * ISO_SECTOR_SIZE); + + return 0; + } + + return max_size; +} + template inline T retrieve_endian_int(const u8* buf) { @@ -539,7 +841,7 @@ static std::optional iso_read_directory_entry(fs::file& entry, utf16.resize(header.file_name_length / 2); - for (usz i = 0; i < utf16.size(); ++i, raw++) + for (usz i = 0; i < utf16.size(); i++, raw++) { utf16[i] = *reinterpret_cast*>(raw); } @@ -661,23 +963,30 @@ u64 iso_fs_metadata::size() const iso_archive::iso_archive(const std::string& path) { m_path = path; - m_file = fs::file(path); - m_dec = std::make_shared(); - if (!m_dec->init(path)) + // "m_path" is updated with the raw device path in case "path" points to a BD drive + fs::get_optical_raw_device(path, &m_path); + + fs::file iso_file(std::make_unique(m_path, fs::read)); + + if (!iso_file || !is_iso_file(iso_file)) { - // Not ISO... TODO: throw something?? + // Not ISO... TODO: throw something? + iso_log.error("iso_archive: Failed to recognize ISO file: %s", path); return; } + // Reset the file position after it was changed by is_iso_file() + iso_file.seek(0); + u8 descriptor_type = -2; bool use_ucs2_decoding = false; do { - const auto descriptor_start = m_file.pos(); + const auto descriptor_start = iso_file.pos(); - descriptor_type = m_file.read(); + descriptor_type = iso_file.read(); // 1 = primary vol descriptor, 2 = joliet SVD if (descriptor_type == 1 || descriptor_type == 2) @@ -685,9 +994,9 @@ iso_archive::iso_archive(const std::string& path) use_ucs2_decoding = descriptor_type == 2; // Skip the rest of descriptor's data - m_file.seek(155, fs::seek_cur); + iso_file.seek(155, fs::seek_cur); - const auto node = iso_read_directory_entry(m_file, use_ucs2_decoding); + const auto node = iso_read_directory_entry(iso_file, use_ucs2_decoding); if (node) { @@ -698,11 +1007,20 @@ iso_archive::iso_archive(const std::string& path) } } - m_file.seek(descriptor_start + ISO_SECTOR_SIZE); + iso_file.seek(descriptor_start + ISO_SECTOR_SIZE); } while (descriptor_type != 255); - iso_form_hierarchy(m_file, m_root, use_ucs2_decoding); + iso_form_hierarchy(iso_file, m_root, use_ucs2_decoding); + + // Only when the archive object is fully set, we can finally initialize the decryption object needing the archive object + m_dec = std::make_shared(); + + if (!m_dec->init(m_path, this)) + { + // TODO: throw something? + return; + } } iso_fs_node* iso_archive::retrieve(const std::string& passed_path) @@ -799,29 +1117,69 @@ bool iso_archive::is_file(const std::string& path) return !file_node->metadata.is_directory; } -iso_file iso_archive::open(const std::string& path) +std::unique_ptr iso_archive::get_iso_file(const std::string& path, bs_t mode, const iso_fs_node& node) { - return iso_file(fs::file(m_path), m_dec, *ensure(retrieve(path))); + if (m_dec->get_enc_type() == iso_encryption_type::NONE) + { + return std::make_unique(path, mode, node); + } + + return std::make_unique(path, mode, node, m_dec); +} + +std::unique_ptr iso_archive::open(const std::string& path) +{ + return get_iso_file(m_path, fs::read, *ensure(retrieve(path))); } psf::registry iso_archive::open_psf(const std::string& path) { - auto* archive_file = retrieve(path); + const auto node = retrieve(path); - if (!archive_file) + if (!node) { return psf::registry(); } - const fs::file psf_file(std::make_unique(fs::file(m_path), m_dec, *archive_file)); + const fs::file psf_file(get_iso_file(m_path, fs::read, *node)); return psf::load_object(psf_file, path); } -iso_file::iso_file(fs::file&& iso_handle, std::shared_ptr iso_dec, const iso_fs_node& node) - : m_file(std::move(iso_handle)), m_dec(iso_dec), m_meta(node.metadata) +iso_file::iso_file(const std::string& path, bs_t mode) { - m_file.seek(node.metadata.extents[0].start * ISO_SECTOR_SIZE); + m_file = fs::file(path, mode); + + if (!m_file) + { + // Should never happen... TODO: throw something? + iso_log.error("iso_file: Failed to open file: %s", path); + return; + } + + m_meta.name = path; + m_meta.extents.push_back({0, m_file.size()}); + + m_file.seek(m_meta.extents[0].start * ISO_SECTOR_SIZE); + + m_raw_device = fs::is_optical_raw_device(path); +} + +iso_file::iso_file(const std::string& path, bs_t mode, const iso_fs_node& node) + : m_meta(node.metadata) +{ + m_file = fs::file(path, mode); + + if (!m_file) + { + // Should never happen... TODO: throw something? + iso_log.error("iso_file: Failed to open file: %s", path); + return; + } + + m_file.seek(m_meta.extents[0].start * ISO_SECTOR_SIZE); + + m_raw_device = fs::is_optical_raw_device(path); } fs::stat_t iso_file::get_stat() @@ -898,29 +1256,27 @@ u64 iso_file::read_at(u64 offset, void* buffer, u64 size) } const u64 archive_first_offset = file_offset(offset); - const u64 total_size = this->size(); - u64 total_read; - // If it's a non-encrypted type - if (m_dec->get_enc_type() == iso_encryption_type::NONE) + // If it's not a raw device + if (!m_raw_device) { - total_read = m_file.read_at(archive_first_offset, buffer, max_size); + u64 total_read = m_file.read_at(archive_first_offset, buffer, max_size); - if (size > total_read && (offset + total_read) < total_size) + if (total_read != max_size) { - total_read += read_at(offset + total_read, reinterpret_cast(buffer) + total_read, size - total_read); + iso_log.error("read_at: %s: Error reading from file (%llu/%llu)", m_meta.name, total_read, max_size); + return 0; } - return total_read; + return max_size; } - // If it's an encrypted type + // If it's a raw device // IMPORTANT NOTE: // - // "iso_file_decryption::decrypt()" method requires that offset and size are multiple of 16 bytes - // (ciphertext block's size) and that a previous ciphertext block (used as IV) is read in case - // offset is not a multiple of ISO_SECTOR_SIZE + // For a raw device, we must use a support buffer aligned (probably enough on multiple of 4), otherwise any read from file will fail. + // For that reason, we don't use directly "buffer" (not guaranteeing any alignment) // // ---------------------------------------------------------------------- // file on ISO archive: | ' ' | @@ -933,50 +1289,36 @@ u64 iso_file::read_at(u64 offset, void* buffer, u64 size) // ------------------------------------------------------------------------------------------------------------------------------------- // ISO archive: | sec 0 | sec 1 |xxxxx######'###########'###########'###########'##xxxxxxxxx| | ... | sec n-1 | sec n | // ------------------------------------------------------------------------------------------------------------------------------------- - // 16 Bytes x block read: | | | | | | | '#######'###########'###########'###########'###| | | | | | | | | | | | | | | // ' ' ' ' // | first sec | inner sec(s) | last sec | const u64 archive_last_offset = archive_first_offset + max_size - 1; iso_sector first_sec, last_sec; - u64 offset_aligned; - u64 offset_aligned_first_out; + void* aligned_buf = get_aligned_buf(); // thread-safe buffer first_sec.lba_address = (archive_first_offset / ISO_SECTOR_SIZE) * ISO_SECTOR_SIZE; first_sec.offset = archive_first_offset % ISO_SECTOR_SIZE; first_sec.size = first_sec.offset + max_size <= ISO_SECTOR_SIZE ? max_size : ISO_SECTOR_SIZE - first_sec.offset; last_sec.lba_address = last_sec.address_aligned = (archive_last_offset / ISO_SECTOR_SIZE) * ISO_SECTOR_SIZE; - //last_sec.offset = last_sec.offset_aligned = 0; // Always 0 so no need to set and use those attributes + // last_sec.offset = last_sec.offset_aligned = 0; // Always 0 so no need to set and use those attributes last_sec.size = (archive_last_offset % ISO_SECTOR_SIZE) + 1; // // First sector // - offset_aligned = first_sec.offset & ~0xF; - offset_aligned_first_out = (first_sec.offset + first_sec.size) & ~0xF; + u64 total_read = m_file.read_at(first_sec.lba_address, aligned_buf, ISO_SECTOR_SIZE); - first_sec.offset_aligned = offset_aligned != 0 ? offset_aligned - 16 : 0; // Eventually include the previous block (used as IV) - first_sec.size_aligned = offset_aligned_first_out != (first_sec.offset + first_sec.size) ? - offset_aligned_first_out + 16 - first_sec.offset_aligned : - offset_aligned_first_out - first_sec.offset_aligned; - first_sec.address_aligned = first_sec.lba_address + first_sec.offset_aligned; + std::memcpy(buffer, &reinterpret_cast(aligned_buf)[first_sec.offset], first_sec.size); - total_read = m_file.read_at(first_sec.address_aligned, &first_sec.buf.data()[first_sec.offset_aligned], first_sec.size_aligned); - - m_dec->decrypt(first_sec.address_aligned, &first_sec.buf.data()[first_sec.offset_aligned], first_sec.size_aligned, m_meta.name); - memcpy(buffer, &first_sec.buf.data()[first_sec.offset], first_sec.size); - - u64 sector_count = (last_sec.lba_address - first_sec.lba_address) / ISO_SECTOR_SIZE + 1; + const u64 sector_count = (last_sec.lba_address - first_sec.lba_address) / ISO_SECTOR_SIZE + 1; if (sector_count < 2) // If no more sector(s) { - if (total_read != first_sec.size_aligned) + if (total_read != ISO_SECTOR_SIZE) { - iso_log.error("read_at: %s: Error reading from file", m_meta.name); - - seek(m_pos, fs::seek_set); + iso_log.error("read_at: %s: Error reading from file (%llu/%llu)", m_meta.name, total_read, ISO_SECTOR_SIZE); return 0; } @@ -987,38 +1329,35 @@ u64 iso_file::read_at(u64 offset, void* buffer, u64 size) // Inner sector(s), if any // - u64 expected_inner_sector_read = 0; - if (sector_count > 2) // If inner sector(s) are present { - u64 inner_sector_size = expected_inner_sector_read = (sector_count - 2) * ISO_SECTOR_SIZE; + u64 sector_offset = 0; - total_read += m_file.read_at(first_sec.lba_address + ISO_SECTOR_SIZE, &reinterpret_cast(buffer)[first_sec.size], inner_sector_size); + for (u64 i = 0; i < sector_count - 2; i++, sector_offset += ISO_SECTOR_SIZE) + { + total_read += m_file.read_at(first_sec.lba_address + ISO_SECTOR_SIZE + sector_offset, aligned_buf, ISO_SECTOR_SIZE); - m_dec->decrypt(first_sec.lba_address + ISO_SECTOR_SIZE, &reinterpret_cast(buffer)[first_sec.size], inner_sector_size, m_meta.name); + std::memcpy(&reinterpret_cast(buffer)[first_sec.size + sector_offset], aligned_buf, ISO_SECTOR_SIZE); + } } // // Last sector // - offset_aligned_first_out = last_sec.size & ~0xF; - last_sec.size_aligned = offset_aligned_first_out != last_sec.size ? offset_aligned_first_out + 16 : offset_aligned_first_out; + total_read += m_file.read_at(last_sec.address_aligned, aligned_buf, ISO_SECTOR_SIZE); - total_read += m_file.read_at(last_sec.address_aligned, last_sec.buf.data(), last_sec.size_aligned); - - m_dec->decrypt(last_sec.address_aligned, last_sec.buf.data(), last_sec.size_aligned, m_meta.name); - memcpy(&reinterpret_cast(buffer)[max_size - last_sec.size], last_sec.buf.data(), last_sec.size); + std::memcpy(&reinterpret_cast(buffer)[max_size - last_sec.size], aligned_buf, last_sec.size); // - // As last, check for an unlikely reading error (decoding also failed due to use of partially initialized buffer) + // As last, check for an unlikely reading error // - if (total_read != first_sec.size_aligned + last_sec.size_aligned + expected_inner_sector_read) + if (total_read != ISO_SECTOR_SIZE + ISO_SECTOR_SIZE + (sector_count - 2) * ISO_SECTOR_SIZE) { - iso_log.error("read_at: %s: Error reading from file", m_meta.name); + iso_log.error("read_at: %s: Error reading from file (%llu/%llu)", m_meta.name, + total_read, ISO_SECTOR_SIZE + ISO_SECTOR_SIZE + (sector_count - 2) * ISO_SECTOR_SIZE); - seek(m_pos, fs::seek_set); return 0; } @@ -1033,11 +1372,10 @@ u64 iso_file::write(const void* /*buffer*/, u64 /*size*/) u64 iso_file::seek(s64 offset, fs::seek_mode whence) { - const s64 total_size = size(); const s64 new_pos = whence == fs::seek_set ? offset : whence == fs::seek_cur ? offset + m_pos : - whence == fs::seek_end ? offset + total_size : -1; + whence == fs::seek_end ? offset + size() : -1; if (new_pos < 0) { @@ -1169,7 +1507,7 @@ std::unique_ptr iso_device::open(const std::string& path, bs_t(fs::file(m_path, mode), m_archive.get_dec(), *node); + return m_archive.get_iso_file(m_archive.path(), mode, *node); } std::unique_ptr iso_device::open_dir(const std::string& path) diff --git a/rpcs3/Loader/ISO.h b/rpcs3/Loader/ISO.h index 52cc33065a..ba39956239 100644 --- a/rpcs3/Loader/ISO.h +++ b/rpcs3/Loader/ISO.h @@ -6,8 +6,7 @@ #include "util/types.hpp" #include "Crypto/aes.h" -bool is_file_iso(const std::string& path); -bool is_file_iso(const fs::file& path); +bool is_iso_file(const std::string& path, u64* size = nullptr, bool* is_raw_device = nullptr); void load_iso(const std::string& path); void unload_iso(); @@ -58,6 +57,8 @@ enum class iso_type_status ERROR_PROCESSING_KEY }; +class iso_archive; + // ISO file decryption class class iso_file_decryption { @@ -66,14 +67,15 @@ private: iso_encryption_type m_enc_type = iso_encryption_type::NONE; std::vector m_region_info; - void reset(); + static iso_type_status get_key(const std::string& key_path, aes_context* aes_ctx = nullptr); + static iso_type_status retrieve_key(iso_archive& archive, std::string& key_path, aes_context& aes_ctx); public: static iso_type_status check_type(const std::string& path, std::string& key_path, aes_context* aes_ctx = nullptr); iso_encryption_type get_enc_type() const { return m_enc_type; } - bool init(const std::string& path); + bool init(const std::string& path, iso_archive* archive = nullptr); bool decrypt(u64 offset, void* buffer, u64 size, const std::string& name); }; @@ -102,10 +104,10 @@ struct iso_fs_node class iso_file : public fs::file_base { -private: +protected: fs::file m_file; - std::shared_ptr m_dec; iso_fs_metadata m_meta; + bool m_raw_device = false; u64 m_pos = 0; std::pair get_extent_pos(u64 pos) const; @@ -114,7 +116,10 @@ private: u64 file_offset(u64 pos) const; public: - iso_file(fs::file&& iso_handle, std::shared_ptr iso_dec, const iso_fs_node& node); + iso_file(const std::string& path, bs_t mode); + iso_file(const std::string& path, bs_t mode, const iso_fs_node& node); + + explicit operator bool() const { return m_file.operator bool(); } fs::stat_t get_stat() override; bool trunc(u64 length) override; @@ -125,6 +130,19 @@ public: u64 size() override; void release() override; + + friend class iso_file_decryption; +}; + +class iso_file_encrypted : public iso_file +{ +private: + std::shared_ptr m_dec; + +public: + iso_file_encrypted(const std::string& path, bs_t mode, const iso_fs_node& node, std::shared_ptr dec); + + u64 read_at(u64 offset, void* buffer, u64 size) override; }; class iso_dir : public fs::dir_base @@ -147,22 +165,23 @@ class iso_archive { private: std::string m_path; - fs::file m_file; - std::shared_ptr m_dec; iso_fs_node m_root {}; + std::shared_ptr m_dec; public: iso_archive(const std::string& path); const std::string& path() const { return m_path; } - const std::shared_ptr get_dec() { return m_dec; } iso_fs_node* retrieve(const std::string& path); bool exists(const std::string& path); bool is_file(const std::string& path); - iso_file open(const std::string& path); + std::unique_ptr get_iso_file(const std::string& path, bs_t mode, const iso_fs_node& node); + std::unique_ptr open(const std::string& path); psf::registry open_psf(const std::string& path); + + friend class iso_file; }; class iso_device : public fs::device_base diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index 32d36d7bd1..6e745f448c 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -551,13 +551,14 @@ void game_list_frame::OnParsingFinished() { std::unique_ptr archive; iso_metadata_cache_entry cache_entry{}; - const bool is_iso = is_file_iso(dir_or_elf); + bool is_raw_device = false; + const bool is_archive = is_iso_file(dir_or_elf, nullptr, &is_raw_device); - if (is_iso) + if (is_archive) { - // Only construct iso_archive (which walks the full directory tree) - // when no valid cache entry exists for this ISO path + mtime. - if (!iso_cache::load(dir_or_elf, cache_entry)) + // Only construct iso_archive (which walks the full directory tree) in case of raw device or + // when no valid cache entry exists for this ISO path + mtime + if (is_raw_device || !iso_cache::load(dir_or_elf, cache_entry)) { archive = std::make_unique(dir_or_elf); } @@ -738,9 +739,9 @@ void game_list_frame::OnParsingFinished() } } - // On cache miss for an ISO, persist the resolved metadata so subsequent - // launches skip iso_archive construction entirely. - if (archive && is_iso) + // With the exception of raw device, on cache miss for an ISO, persist the resolved metadata so subsequent + // launches skip iso_archive construction entirely + if (archive && is_archive && !is_raw_device) { fs::stat_t iso_stat{}; if (fs::get_stat(dir_or_elf, iso_stat)) @@ -755,11 +756,11 @@ void game_list_frame::OnParsingFinished() if (game.icon_in_archive) { auto icon_file = archive->open(game.info.icon_path); - const auto icon_size = icon_file.size(); + const auto icon_size = icon_file->size(); if (icon_size > 0) { cache_entry.icon_data.resize(icon_size); - icon_file.read(cache_entry.icon_data.data(), icon_size); + icon_file->read(cache_entry.icon_data.data(), icon_size); } } @@ -854,7 +855,11 @@ void game_list_frame::OnParsingFinished() if (entry.is_from_yml) { - if (fs::is_file(entry.path + "/PARAM.SFO")) + if (is_iso_file(entry.path)) + { + push_path(entry.path, legit_paths); + } + else if (fs::is_file(entry.path + "/PARAM.SFO")) { push_path(entry.path, legit_paths); } @@ -887,10 +892,6 @@ void game_list_frame::OnParsingFinished() add_disc_dir(entry.path, legit_paths); } - else if (is_file_iso(entry.path)) - { - push_path(entry.path, legit_paths); - } else { game_list_log.trace("Invalid game path registered: %s", entry.path); diff --git a/rpcs3/rpcs3qt/game_list_grid.cpp b/rpcs3/rpcs3qt/game_list_grid.cpp index 903fdf016a..04dd3a4e98 100644 --- a/rpcs3/rpcs3qt/game_list_grid.cpp +++ b/rpcs3/rpcs3qt/game_list_grid.cpp @@ -124,7 +124,7 @@ void game_list_grid::populate( check_iso |= !fs::exists(game->info.audio_path); } - if (check_iso && is_file_iso(game->info.path)) + if (check_iso && is_iso_file(game->info.path)) { item->set_iso_path(game->info.path); } diff --git a/rpcs3/rpcs3qt/game_list_table.cpp b/rpcs3/rpcs3qt/game_list_table.cpp index ec88561580..ca157c5183 100644 --- a/rpcs3/rpcs3qt/game_list_table.cpp +++ b/rpcs3/rpcs3qt/game_list_table.cpp @@ -283,12 +283,8 @@ void game_list_table::populate( // Do not report size of apps inside /dev_flash (it does not make sense to do so) game->info.size_on_disk = 0; } - else if (is_file_iso(game->info.path)) + else if (is_iso_file(game->info.path, &game->info.size_on_disk)) // If iso file, game->info.size_on_disk is also set { - fs::stat_t iso_stat; - fs::get_stat(game->info.path, iso_stat); - - game->info.size_on_disk = iso_stat.size; } else { @@ -317,7 +313,7 @@ void game_list_table::populate( check_iso |= !fs::exists(game->info.audio_path); } - if (check_iso && is_file_iso(game->info.path)) + if (check_iso && is_iso_file(game->info.path)) { icon_item->set_iso_path(game->info.path); } diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index e44bed7f81..b651ea8799 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -4080,7 +4080,7 @@ main_window::drop_type main_window::IsValidFile(const QMimeData& md, QStringList const QString suffix_lo = info.suffix().toLower(); // check for directories first, only valid if all other paths led to directories until now. - if (info.isDir() || is_file_iso(path.toStdString())) + if (info.isDir() || is_iso_file(path.toStdString())) { if (type != drop_type::drop_dir && type != drop_type::drop_error) { diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index 7ef0317ec2..b648688bf8 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -403,7 +403,7 @@ namespace gui // Get Icon for the gs_frame from path. this handles presumably all possible use cases std::vector path_list; - const bool is_archive = is_file_iso(path); + const bool is_archive = is_iso_file(path); if (is_archive) { icon_path = "PS3_GAME/ICON0.PNG"; @@ -708,11 +708,15 @@ namespace gui bool load_iso_icon(QPixmap& icon, const std::string& icon_path, const std::string& archive_path) { if (icon_path.empty() || archive_path.empty()) return false; - if (!is_file_iso(archive_path)) return false; - // Check cache first — avoids constructing a full iso_archive just for the icon. + bool is_raw_device = false; + const bool is_archive = is_iso_file(archive_path, nullptr, &is_raw_device); + + if (!is_archive) return false; + + // With the exception of raw device, check cache first — avoids constructing a full iso_archive just for the icon. iso_metadata_cache_entry cache_entry{}; - if (iso_cache::load(archive_path, cache_entry) && !cache_entry.icon_data.empty()) + if (!is_raw_device && iso_cache::load(archive_path, cache_entry) && !cache_entry.icon_data.empty()) { const QByteArray data(reinterpret_cast(cache_entry.icon_data.data()), static_cast(cache_entry.icon_data.size())); @@ -723,11 +727,11 @@ namespace gui if (!archive.exists(icon_path)) return false; auto icon_file = archive.open(icon_path); - const auto icon_size = icon_file.size(); + const auto icon_size = icon_file->size(); if (icon_size == 0) return false; QByteArray data(icon_size, 0); - icon_file.read(data.data(), icon_size); + icon_file->read(data.data(), icon_size); return icon.loadFromData(data); } diff --git a/rpcs3/rpcs3qt/qt_video_source.cpp b/rpcs3/rpcs3qt/qt_video_source.cpp index 760369ad74..14cda830fb 100644 --- a/rpcs3/rpcs3qt/qt_video_source.cpp +++ b/rpcs3/rpcs3qt/qt_video_source.cpp @@ -113,11 +113,11 @@ void qt_video_source::init_movie() { iso_archive archive(m_iso_path); auto movie_file = archive.open(m_video_path.toStdString()); - const auto movie_size = movie_file.size(); + const auto movie_size = movie_file->size(); if (movie_size == 0) return; m_video_data = QByteArray(movie_size, 0); - movie_file.read(m_video_data.data(), movie_size); + movie_file->read(m_video_data.data(), movie_size); m_video_buffer = std::make_unique(&m_video_data); m_video_buffer->open(QIODevice::ReadOnly); @@ -156,14 +156,14 @@ void qt_video_source::init_movie() { iso_archive archive(m_iso_path); auto movie_file = archive.open(m_video_path.toStdString()); - const auto movie_size = movie_file.size(); + const auto movie_size = movie_file->size(); if (movie_size == 0) { return; } m_video_data = QByteArray(movie_size, 0); - movie_file.read(m_video_data.data(), movie_size); + movie_file->read(m_video_data.data(), movie_size); } if (m_video_data.isEmpty()) @@ -280,12 +280,12 @@ void qt_video_source::start_audio() { iso_archive archive(m_iso_path); auto audio_file = archive.open(m_audio_path.toStdString()); - const auto audio_size = audio_file.size(); + const auto audio_size = audio_file->size(); if (audio_size == 0) return; std::unique_ptr old_audio_data = std::move(audio.data); audio.data = std::make_unique(audio_size, 0); - audio_file.read(audio.data->data(), audio_size); + audio_file->read(audio.data->data(), audio_size); if (!audio.buffer) { diff --git a/rpcs3/rpcs3qt/shortcut_utils.cpp b/rpcs3/rpcs3qt/shortcut_utils.cpp index d7b5586e75..2112dfe0e9 100644 --- a/rpcs3/rpcs3qt/shortcut_utils.cpp +++ b/rpcs3/rpcs3qt/shortcut_utils.cpp @@ -72,7 +72,7 @@ namespace gui::utils return false; } - const bool is_archive = is_file_iso(path); + const bool is_archive = is_iso_file(path); QPixmap icon; if (!load_icon(icon, src_icon_path, is_archive ? path : "")) @@ -478,7 +478,7 @@ namespace gui::utils std::string gameid_token_value; const std::string dev_flash = g_cfg_vfs.get_dev_flash(); - const bool is_iso = is_file_iso(game->info.path); + const bool is_archive = is_iso_file(game->info.path); std::shared_ptr archive; const auto file_exists = [&archive](const std::string& path) @@ -486,7 +486,7 @@ namespace gui::utils return archive ? archive->is_file(path) : fs::is_file(path); }; - if (is_iso) + if (is_archive) { gameid_token_value = game->info.serial; archive = std::make_shared(game->info.path); @@ -542,7 +542,7 @@ namespace gui::utils if (location == shortcut_location::steam) { // Try to find a nice banner for steam - const std::string sfo_dir = is_iso ? "PS3_GAME" : rpcs3::utils::get_sfo_dir_from_game_path(game->info.path); + const std::string sfo_dir = is_archive ? "PS3_GAME" : rpcs3::utils::get_sfo_dir_from_game_path(game->info.path); for (const std::string& filename : {"PIC1.PNG"s, "PIC3.PNG"s, "PIC0.PNG"s, "PIC2.PNG"s, "ICON0.PNG"s}) {