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 `<rpcs3>/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`
This commit is contained in:
Antonino Di Guardo 2026-04-29 04:42:49 +02:00 committed by GitHub
parent e05d359721
commit b212935c70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 668 additions and 212 deletions

View file

@ -38,6 +38,17 @@ static std::unique_ptr<wchar_t[]> to_wchar(std::string_view source)
// Buffer for max possible output length
std::unique_ptr<wchar_t[]> 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<u64> 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:\<nul>B:\<nul>C:\<nul><nul>
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<open_mode> 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<windows_file>(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<open_mode> mode)
g_tls_error = fs::error::isdir;
return;
}
g_tls_error = to_error(last_error);
return;
}

View file

@ -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);

View file

@ -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<iso_archive> archive;
if (is_file_iso(path))
if (is_iso_file(path))
{
archive = std::make_unique<iso_archive>(path);
}

View file

@ -10,6 +10,7 @@
#include <cmath>
#include <filesystem>
#include <stack>
#include <cstdlib>
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<u8, ISO_SECTOR_SIZE> 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<iso_file>(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<u8, 16>& 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<std::string, 4> 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<std::string, std::string> 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<u8, ISO_SECTOR_SIZE> enc_sec;
std::array<u8, ISO_SECTOR_SIZE> 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<fs::dir_entry> 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<std::string, 4> 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<iso_file>(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<u8, ISO_SECTOR_SIZE * 2> sec0_sec1 {};
// Reset the file position after it was changed by is_iso_file()
iso_file.seek(0);
std::array<u8, ISO_SECTOR_SIZE * 2> 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<unsigned char*>(buffer), size);
decrypt_data(m_aes_dec, offset, reinterpret_cast<unsigned char*>(buffer), reinterpret_cast<unsigned char*>(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<fs::open_mode> mode, const iso_fs_node& node, std::shared_ptr<iso_file_decryption> 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<u8*>(aligned_buf)[first_sec.offset_aligned], first_sec.size_aligned);
m_dec->decrypt(first_sec.address_aligned, &reinterpret_cast<u8*>(aligned_buf)[first_sec.offset_aligned], first_sec.size_aligned, m_meta.name);
std::memcpy(buffer, &reinterpret_cast<u8*>(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<u8*>(buffer)[first_sec.size], inner_sector_size);
m_dec->decrypt(first_sec.lba_address + ISO_SECTOR_SIZE, &reinterpret_cast<u8*>(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<u8*>(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<u8*>(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<typename T>
inline T retrieve_endian_int(const u8* buf)
{
@ -539,7 +841,7 @@ static std::optional<iso_fs_metadata> 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<const be_t<u16>*>(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<iso_file_decryption>();
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<iso_file>(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<u8>();
descriptor_type = iso_file.read<u8>();
// 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<iso_file_decryption>();
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<fs::file_base> iso_archive::get_iso_file(const std::string& path, bs_t<fs::open_mode> 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<iso_file>(path, mode, node);
}
return std::make_unique<iso_file_encrypted>(path, mode, node, m_dec);
}
std::unique_ptr<fs::file_base> 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<iso_file>(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_file_decryption> 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<fs::open_mode> 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<fs::open_mode> 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<u8*>(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<u8*>(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<u8*>(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<u8*>(buffer)[first_sec.size], inner_sector_size, m_meta.name);
std::memcpy(&reinterpret_cast<u8*>(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<u8*>(buffer)[max_size - last_sec.size], last_sec.buf.data(), last_sec.size);
std::memcpy(&reinterpret_cast<u8*>(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<fs::file_base> iso_device::open(const std::string& path, bs_t<fs
return nullptr;
}
return std::make_unique<iso_file>(fs::file(m_path, mode), m_archive.get_dec(), *node);
return m_archive.get_iso_file(m_archive.path(), mode, *node);
}
std::unique_ptr<fs::dir_base> iso_device::open_dir(const std::string& path)

View file

@ -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<iso_region_info> 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<iso_file_decryption> m_dec;
iso_fs_metadata m_meta;
bool m_raw_device = false;
u64 m_pos = 0;
std::pair<u64, iso_extent_info> 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_file_decryption> iso_dec, const iso_fs_node& node);
iso_file(const std::string& path, bs_t<fs::open_mode> mode);
iso_file(const std::string& path, bs_t<fs::open_mode> 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<iso_file_decryption> m_dec;
public:
iso_file_encrypted(const std::string& path, bs_t<fs::open_mode> mode, const iso_fs_node& node, std::shared_ptr<iso_file_decryption> 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<iso_file_decryption> m_dec;
iso_fs_node m_root {};
std::shared_ptr<iso_file_decryption> m_dec;
public:
iso_archive(const std::string& path);
const std::string& path() const { return m_path; }
const std::shared_ptr<iso_file_decryption> 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<fs::file_base> get_iso_file(const std::string& path, bs_t<fs::open_mode> mode, const iso_fs_node& node);
std::unique_ptr<fs::file_base> open(const std::string& path);
psf::registry open_psf(const std::string& path);
friend class iso_file;
};
class iso_device : public fs::device_base

View file

@ -551,13 +551,14 @@ void game_list_frame::OnParsingFinished()
{
std::unique_ptr<iso_archive> 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<iso_archive>(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);

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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)
{

View file

@ -403,7 +403,7 @@ namespace gui
// Get Icon for the gs_frame from path. this handles presumably all possible use cases
std::vector<std::string> 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<const char*>(cache_entry.icon_data.data()),
static_cast<qsizetype>(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);
}

View file

@ -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<QBuffer>(&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<QByteArray> old_audio_data = std::move(audio.data);
audio.data = std::make_unique<QByteArray>(audio_size, 0);
audio_file.read(audio.data->data(), audio_size);
audio_file->read(audio.data->data(), audio_size);
if (!audio.buffer)
{

View file

@ -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<iso_archive> 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<iso_archive>(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})
{