Files
jpolo1224 2f0c63ac1b ISO: stop treating a short magic read as "not an ISO"
Reverts upstream 8a6c96745, taken in the ROP remap merge, back to the 0.8
behaviour. The check is sound on a desktop filesystem, but on Android the disc is
read through SAF/content URIs and this predicate decides how a title gets
mounted: System.cpp:1572, 1589, 1764 and 1878 all branch on it, as does
rpcsx-android.cpp:3935. A short read there does not fail the boot outright, it
silently routes the title down a different mount path -- which is what a game
that reaches its first loading screen and never leaves it looks like.

Not identical to 0.8: magic[] is zero-initialised. The original compared it
without checking the read at all, so a failed read compared uninitialised stack.
Zeroing restores "do not reject on a short read" without the undefined behaviour,
and a genuinely non-ISO file still fails the CD001 test.

iso_archive::is_valid() stays. It comes from a different upstream commit
(d6d5c6082), System.cpp calls it, and it is not part of this predicate.

Reported against NASCAR 2011, which reached the menus before the merge and now
sticks on the first loading screen. NOT confirmed as the cause -- the reporter's
build predates the local commits, and the other public candidates in that window
are the Adreno compute group size and the driver pipeline cache work.
2026-08-21 19:50:32 -04:00

1623 lines
45 KiB
C++

#include "stdafx.h"
#include "ISO.h"
#include "Emu/VFS.h"
#include "Emu/system_utils.hpp"
#include "Crypto/utils.h"
#include <codecvt>
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <stack>
#include <cstdlib>
LOG_CHANNEL(sys_log, "SYS");
LOG_CHANNEL(iso_log, "ISO");
struct iso_sector
{
u64 lba_address;
u64 offset;
u64 size;
u64 address_aligned;
u64 offset_aligned;
u64 size_aligned;
};
static void* get_aligned_buf()
{
static thread_local struct aligned_buf
{
void* buf;
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;
}
static bool is_iso_file(iso_file& file, u64* size = nullptr)
{
if (!file || file.size() < 32768ULL + 6)
{
return false;
}
// Zero-initialised, and a short read is NOT treated as "not an ISO".
//
// Upstream's 8a6c96745 added a `!= 5` early return here, which this reverts to the 0.8
// behaviour. The check is sound on a desktop filesystem, but on Android the disc is read
// through SAF/content URIs, and this predicate decides how the game gets mounted --
// System.cpp:1572/1589/1764/1878 and rpcsx-android.cpp:3935 all branch on it. A single
// short read there does not fail the boot outright; it silently routes the title down a
// different mount path, which is what a game reaching its first loading screen and never
// leaving it looks like.
//
// The zero-init is kept from the upstream version rather than restoring 0.8 exactly: the
// original compared `magic` without checking the read at all, so a failed read compared
// uninitialised stack. Zeroing gives the old "don't reject on a short read" behaviour
// without the undefined behaviour -- a genuinely non-ISO file still fails the CD001 test.
char magic[5]{};
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_file(path))
{
return false;
}
if (is_raw_device)
{
*is_raw_device = raw_device;
}
iso_file file(new_path);
return is_iso_file(file, size);
}
// Convert 4 bytes in big-endian format to an unsigned integer
static u32 char_arr_BE_to_uint(const u8* arr)
{
return arr[0] << 24 | arr[1] << 16 | arr[2] << 8 | arr[3];
}
// Reset the iv to a particular LBA
static void reset_iv(std::array<u8, 16>& iv, u32 lba)
{
memset(iv.data(), 0, 12);
iv[12] = (lba & 0xFF000000) >> 24;
iv[13] = (lba & 0x00FF0000) >> 16;
iv[14] = (lba & 0x0000FF00) >> 8;
iv[15] = (lba & 0x000000FF) >> 0;
}
// Main function that will decrypt the sector(s)
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
//if (size == 0)
//{
// return false;
//}
//if ((size % 16) != 0)
//{
// iso_log.error("decrypt_data: Requested ciphertext blocks' size must be a multiple of 16 (%llu)", size);
// return;
//}
u32 cur_sector_lba = static_cast<u32>(offset / ISO_SECTOR_SIZE); // First sector's LBA
const u32 sector_count = static_cast<u32>((offset + size - 1) / ISO_SECTOR_SIZE) - cur_sector_lba + 1;
const u64 sector_offset = offset % ISO_SECTOR_SIZE;
std::array<u8, 16> iv;
u64 cur_offset;
u64 cur_size;
// If the offset is not at the beginning of a sector, the first 16 bytes in the buffer
// represents the IV for decrypting the next data in the buffer.
// Otherwise, the IV is based on sector's LBA
if (sector_offset != 0)
{
std::memcpy(iv.data(), buffer, 16);
cur_offset = 16;
}
else
{
reset_iv(iv, cur_sector_lba);
cur_offset = 0;
}
cur_size = sector_offset + size <= ISO_SECTOR_SIZE ? size : ISO_SECTOR_SIZE - sector_offset;
cur_size -= cur_offset;
// Partial (or even full) first sector
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;
}
if (sector_count < 2) // If no more sector(s)
{
return true;
}
cur_offset += cur_size;
const u32 inner_sector_count = sector_count > 2 ? sector_count - 2 : 0; // Remove first and last sector
// Inner sector(s), if any
for (u32 i = 0; i < inner_sector_count; i++)
{
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], &out_buffer[cur_offset]) != 0)
{
iso_log.error("decrypt_data: Error decrypting data on inner sector(s) read");
return false;
}
cur_offset += ISO_SECTOR_SIZE;
}
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], &out_buffer[cur_offset]) != 0)
{
iso_log.error("decrypt_data: Error decrypting data on last sector read");
return false;
}
return true;
}
iso_type_status iso_file_decryption::get_key(const std::string& key_path, aes_context* aes_ctx)
{
fs::file key_file(key_path);
// If no ".dkey" and ".key" file exists
if (!key_file)
{
return iso_type_status::ERROR_OPENING_KEY;
}
std::array<char, 32> key_str {};
std::array<u8, 16> key {};
const u64 key_len = key_file.read(key_str.data(), key_str.size());
if (key_len == key_str.size() || key_len == key.size())
{
// If the key read from the key file is 16 bytes long instead of 32, consider the file as
// binary (".key") and so not needing any further conversion from hex string to bytes
if (key_len == key.size())
{
std::memcpy(key.data(), key_str.data(), key.size());
}
else
{
std::string error;
hex_to_bytes(key.data(), std::string_view(key_str.data(), key_str.size()), key_str.size(), &error);
if (!error.empty())
{
iso_log.error("get_key(%s): %s", key_path, error);
return iso_type_status::ERROR_PROCESSING_KEY;
}
}
aes_context aes_dec {};
// If "aes_ctx" not requested
if (!aes_ctx)
{
aes_ctx = &aes_dec;
}
// Create the decryption context. If the context is successfully created, fill in "aes_ctx"
// (if requested) and return REDUMP_ISO
if (aes_setkey_dec(aes_ctx, key.data(), 128) == 0)
{
return iso_type_status::REDUMP_ISO;
}
}
return iso_type_status::ERROR_PROCESSING_KEY;
}
iso_type_status iso_file_decryption::retrieve_key(iso_archive& archive, std::string& key_path, aes_context& aes_ctx)
{
//
// Find the first existing file in the archive present on the list of well known encrypted files to use for testing a matching key
//
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)
{
if (iso_fs_node* _node = archive.retrieve(magic.first))
{
magic_value = magic.second;
node = _node;
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))
{
if (key_path)
{
*key_path = path;
}
return get_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)
//
iso_file iso_file(path);
if (!is_iso_file(iso_file))
{
iso_log.error("init: Failed to recognize ISO file: '%s'", path);
return false;
}
// Reset the file position after it was changed by is_iso_file()
iso_file.seek(0, fs::seek_set);
std::array<u8, ISO_SECTOR_SIZE * 2> sec0_sec1;
if (iso_file.size() < sec0_sec1.size())
{
iso_log.error("init: Found only %llu sector(s) (minimum required is 2): '%s'", iso_file.size(), path);
return false;
}
if (iso_file.read(sec0_sec1.data(), sec0_sec1.size()) != sec0_sec1.size())
{
iso_log.error("init: Failed to read file: '%s'", path);
return false;
}
// NOTE:
//
// Following checks and assigned values are based on PS3 ISO specification.
// E.g. all even regions (0, 2, 4 etc.) are always unencrypted while the odd ones are encrypted
const u32 region_count = char_arr_BE_to_uint(sec0_sec1.data());
// 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 (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++)
{
// Store the region information in address format
m_region_info[i].encrypted = (i % 2 == 1);
m_region_info[i].region_first_addr = (i == 0 ? 0ULL : m_region_info[i - 1].region_last_addr + 1ULL);
m_region_info[i].region_last_addr = (static_cast<u64>(char_arr_BE_to_uint(sec0_sec1.data() + 12 + (i * 4)))
- (i % 2 == 1 ? 1ULL : 0ULL)) * ISO_SECTOR_SIZE + ISO_SECTOR_SIZE - 1ULL;
}
//
// Check for Redump type
//
iso_type_status status;
std::string key_path;
// 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:
iso_log.warning("init: Failed to open, or missing, key file: '%s'", key_path);
break;
case iso_type_status::ERROR_PROCESSING_KEY:
iso_log.error("init: Failed to process key file: '%s'", key_path);
break;
default:
break;
}
//
// Check for 3k3y type
//
// If encryption type is still set to NONE
if (m_enc_type == iso_encryption_type::NONE)
{
// The 3k3y watermarks located at offset 0xF70: (D|E)ncrypted 3K BLD
static const unsigned char k3k3y_enc_watermark[16] =
{0x45, 0x6E, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x20, 0x33, 0x4B, 0x20, 0x42, 0x4C, 0x44};
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 (std::memcmp(&k3k3y_enc_watermark[0], &sec0_sec1[0xF70], sizeof(k3k3y_enc_watermark)) == 0)
{
// Grab D1 from the 3k3y sector
unsigned char key[16];
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};
unsigned char iv_d1[] = {0x69, 0x47, 0x47, 0x72, 0xaf, 0x6f, 0xda, 0xb3, 0x42, 0x74, 0x3a, 0xef, 170, 0x18, 0x62, 0x87};
aes_context aes_d1;
if (aes_setkey_enc(&aes_d1, key_d1, 128) == 0)
{
if (aes_crypt_cbc(&aes_d1, AES_ENCRYPT, 16, &iv_d1[0], key, key) == 0)
{
if (aes_setkey_dec(&m_aes_dec, key, 128) == 0)
{
m_enc_type = iso_encryption_type::ENC_3K3Y; // SET ENCRYPTION TYPE: ENC_3K3Y
}
}
}
if (m_enc_type == iso_encryption_type::NONE) // If encryption type was not set to ENC_3K3Y for any reason
{
iso_log.error("init: Failed to set encryption type to ENC_3K3Y: '%s'", path);
}
}
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
}
}
switch (m_enc_type)
{
case iso_encryption_type::REDUMP:
iso_log.warning("init: Set 'enc type': REDUMP, 'reg count': %u: '%s'", m_region_info.size(), path);
break;
case iso_encryption_type::ENC_3K3Y:
iso_log.warning("init: Set 'enc type': ENC_3K3Y, 'reg count': %u: '%s'", m_region_info.size(), path);
break;
case iso_encryption_type::DEC_3K3Y:
iso_log.warning("init: Set 'enc type': DEC_3K3Y, 'reg count': %u: '%s'", m_region_info.size(), path);
break;
case iso_encryption_type::NONE: // If encryption type was not set for any reason
iso_log.warning("init: Set 'enc type': NONE, 'reg count': %u: '%s'", m_region_info.size(), path);
break;
}
return true;
}
bool iso_file_decryption::decrypt(u64 offset, void* buffer, u64 size, const std::string& name)
{
// If it's a non-encrypted type, nothing more to do
if (m_enc_type == iso_encryption_type::NONE)
{
return true;
}
// If it's a 3k3y ISO and data at offset 0xF70 is being requested, we should null it out
if (m_enc_type == iso_encryption_type::DEC_3K3Y || m_enc_type == iso_encryption_type::ENC_3K3Y)
{
if (offset + size >= 0xF70ULL && offset <= 0x1070ULL)
{
// Zero out the 0xF70 - 0x1070 overlap
unsigned char* buf = reinterpret_cast<unsigned char*>(buffer);
unsigned char* buf_overlap_start = offset < 0xF70ULL ? buf + 0xF70ULL - offset : buf;
memset(buf_overlap_start, 0x00, offset + size < 0x1070ULL ? size - (buf_overlap_start - buf) : 0x100ULL - (buf_overlap_start - buf));
}
// If it's a decrypted ISO then return, otherwise go on to the decryption logic
if (m_enc_type == iso_encryption_type::DEC_3K3Y)
{
return true;
}
}
// If it's an encrypted type, check if the request lies in an encrypted range
for (const iso_region_info& info : m_region_info)
{
if (offset >= info.region_first_addr && offset <= info.region_last_addr)
{
// We found the region, decrypt if needed
if (!info.encrypted)
{
return true;
}
// Decrypt the region before sending it back
decrypt_data(m_aes_dec, offset, reinterpret_cast<unsigned char*>(buffer), reinterpret_cast<unsigned char*>(buffer), size);
return true;
}
}
iso_log.error("decrypt: %s: LBA request wasn't in the 'm_region_info' for an encrypted ISO? - RP: 0x%lx, RC: 0x%lx, LR: (0x%016lx - 0x%016lx)",
name,
offset,
static_cast<unsigned long int>(m_region_info.size()),
static_cast<unsigned long int>(!m_region_info.empty() ? m_region_info.back().region_first_addr : 0),
static_cast<unsigned long int>(!m_region_info.empty() ? m_region_info.back().region_last_addr : 0));
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 |
u64 max_size = std::min(size, local_extent_remaining(offset));
if (max_size == 0)
{
return 0;
}
const u64 total_size = this->size();
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 - O: %llu (%llu), S: %llu/%llu/%llu (%llu), TR: %llu", m_meta.name,
offset, first_sec.address_aligned, first_sec.size_aligned, max_size, size, total_size, total_read);
return 0;
}
// If present, read the remaining chunk of data on next extent
if (size > max_size && (offset + max_size) < total_size)
{
iso_log.warning("read_at: %s: Extent limit reached reading from file (%llu/%llu)", m_meta.name, max_size, size);
max_size += read_at(offset + max_size, &reinterpret_cast<u8*>(buffer)[max_size], size - max_size);
}
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 - O: %llu (%llu), S: %llu/%llu/%llu (%llu), TR: %llu/%llu", m_meta.name,
offset, first_sec.address_aligned, last_sec.size_aligned, max_size, size, total_size,
total_read, ISO_SECTOR_SIZE + ISO_SECTOR_SIZE + (sector_count - 2) * ISO_SECTOR_SIZE);
return 0;
}
// If present, read the remaining chunk of data on next extent
if (size > max_size && (offset + max_size) < total_size)
{
iso_log.warning("read_at: %s: Extent limit reached reading from file (%llu/%llu)", m_meta.name, max_size, size);
max_size += read_at(offset + max_size, &reinterpret_cast<u8*>(buffer)[max_size], size - max_size);
}
return max_size;
}
template<typename T>
inline T retrieve_endian_int(const u8* buf)
{
T out {};
if constexpr (std::endian::little == std::endian::native)
{
// First half = little-endian copy
std::memcpy(&out, buf, sizeof(T));
}
else
{
// Second half = big-endian copy
std::memcpy(&out, buf + sizeof(T), sizeof(T));
}
return out;
}
// Assumed that directory entry is at file head
static std::optional<iso_fs_metadata> iso_read_directory_entry(fs::file& entry, bool names_in_ucs2 = false)
{
const auto start_pos = entry.pos();
const u8 entry_length = entry.read<u8>();
if (entry_length == 0)
{
return std::nullopt;
}
// Batch this set of file reads. This reduces overall time spent in iso_read_directory_entry by ~41%
#pragma pack(push, 1)
struct iso_entry_header
{
//u8 entry_length; // Handled separately
u8 extended_attribute_length;
u8 start_sector[8];
u8 file_size[8];
u8 year;
u8 month;
u8 day;
u8 hour;
u8 minute;
u8 second;
u8 timezone_value;
u8 flags;
u8 file_unit_size;
u8 interleave;
u8 volume_sequence_number[4];
u8 file_name_length;
//u8 file_name[file_name_length]; // Handled separately
};
#pragma pack(pop)
static_assert(sizeof(iso_entry_header) == 32);
const iso_entry_header header = entry.read<iso_entry_header>();
const u32 start_sector = retrieve_endian_int<u32>(header.start_sector);
const u32 file_size = retrieve_endian_int<u32>(header.file_size);
std::tm file_date = {};
file_date.tm_year = header.year;
file_date.tm_mon = header.month - 1;
file_date.tm_mday = header.day;
file_date.tm_hour = header.hour;
file_date.tm_min = header.minute;
file_date.tm_sec = header.second;
const s16 timezone_value = header.timezone_value;
const s16 timezone_offset = (timezone_value - 50) * 15 * 60;
const std::time_t date_time = std::mktime(&file_date) + timezone_offset;
// 2nd flag bit indicates whether a given fs node is a directory
const bool is_directory = header.flags & 0b00000010;
const bool has_more_extents = header.flags & 0b10000000;
std::string file_name;
entry.read(file_name, header.file_name_length);
if (header.file_name_length == 1 && file_name[0] == 0)
{
file_name = ".";
}
else if (file_name == "\1")
{
file_name = "..";
}
else if (names_in_ucs2) // For strings in joliet descriptor
{
// Characters are stored in big endian format
const be_t<u16>* raw = utils::bless<const be_t<u16>>(file_name.data());
std::u16string utf16;
utf16.resize(header.file_name_length / 2);
for (usz i = 0; i < utf16.size(); i++)
{
utf16[i] = raw[i];
}
file_name = utf16_to_utf8(utf16);
}
if (file_name.ends_with(";1"))
{
file_name.erase(file_name.end() - 2, file_name.end());
}
if (header.file_name_length > 1 && file_name.ends_with("."))
{
file_name.pop_back();
}
// Skip the rest of the entry
entry.seek(entry_length + start_pos);
return iso_fs_metadata
{
.name = std::move(file_name),
.time = date_time,
.is_directory = is_directory,
.has_multiple_extents = has_more_extents,
.extents =
{
iso_extent_info
{
.start = start_sector,
.size = file_size
}
}
};
}
static void iso_form_hierarchy(fs::file& file, iso_fs_node& node, bool use_ucs2_decoding = false, const std::string& parent_path = "")
{
if (!node.metadata.is_directory)
{
return;
}
std::vector<usz> multi_extent_node_indices;
// Assuming the directory spans a single extent
const auto& directory_extent = node.metadata.extents[0];
const u64 end_pos = (directory_extent.start * ISO_SECTOR_SIZE) + directory_extent.size;
file.seek(directory_extent.start * ISO_SECTOR_SIZE);
while (file.pos() < end_pos)
{
auto entry = iso_read_directory_entry(file, use_ucs2_decoding);
if (!entry)
{
const u64 new_sector = (file.pos() / ISO_SECTOR_SIZE) + 1;
file.seek(new_sector * ISO_SECTOR_SIZE);
continue;
}
bool extent_added = false;
// Find previous extent and merge into it, otherwise we push this node's index
for (usz index : multi_extent_node_indices)
{
auto& selected_node = ::at32(node.children, index);
if (selected_node->metadata.name == entry->name)
{
// Merge into selected_node
selected_node->metadata.extents.push_back(entry->extents[0]);
extent_added = true;
break;
}
}
if (extent_added)
{
continue;
}
if (entry->has_multiple_extents)
{
// Haven't pushed entry to node.children yet so node.children::size() == entry_index
multi_extent_node_indices.push_back(node.children.size());
}
node.children.push_back(std::make_unique<iso_fs_node>(iso_fs_node{
.metadata = std::move(*entry)
}));
}
for (auto& child_node : node.children)
{
if (child_node->metadata.name != "." && child_node->metadata.name != "..")
{
iso_form_hierarchy(file, *child_node, use_ucs2_decoding, parent_path + "/" + node.metadata.name);
}
}
}
u64 iso_fs_metadata::size() const
{
u64 total_size = 0;
for (const auto& extent : extents)
{
total_size += extent.size;
}
return total_size;
}
iso_archive::iso_archive(const std::string& path)
{
m_path = 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);
if (!is_iso_file(m_path))
{
// Not ISO... TODO: throw something?
iso_log.error("iso_archive: Failed to recognize ISO file: '%s'", path);
return;
}
fs::file iso_file(std::make_unique<iso_file>(m_path));
u8 descriptor_type = -2;
bool use_ucs2_decoding = false;
do
{
const auto descriptor_start = iso_file.pos();
descriptor_type = iso_file.read<u8>();
// 1 = primary vol descriptor, 2 = joliet SVD
if (descriptor_type == 1 || descriptor_type == 2)
{
use_ucs2_decoding = descriptor_type == 2;
// Skip the rest of descriptor's data
iso_file.seek(155, fs::seek_cur);
const auto node = iso_read_directory_entry(iso_file, use_ucs2_decoding);
if (node)
{
m_root = iso_fs_node
{
.metadata = node.value()
};
}
}
iso_file.seek(descriptor_start + ISO_SECTOR_SIZE);
}
while (descriptor_type != 255);
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)
{
if (passed_path.empty())
{
return nullptr;
}
const std::string path = std::filesystem::path(passed_path).string();
const std::string_view path_sv = path;
usz start = 0;
usz end = path_sv.find_first_of(fs::delim);
std::stack<iso_fs_node*> search_stack;
search_stack.push(&m_root);
do
{
if (search_stack.empty())
{
return nullptr;
}
const auto* top_entry = search_stack.top();
if (end == umax)
{
end = path.size();
}
const std::string_view path_component = path_sv.substr(start, end - start);
bool found = false;
if (path_component == ".")
{
found = true;
}
else if (path_component == "..")
{
search_stack.pop();
found = true;
}
else
{
for (const auto& entry : top_entry->children)
{
if (entry->metadata.name == path_component)
{
search_stack.push(entry.get());
found = true;
break;
}
}
}
if (!found)
{
return nullptr;
}
start = end + 1;
end = path_sv.find_first_of(fs::delim, start);
}
while (start < path.size());
if (search_stack.empty())
{
return nullptr;
}
return search_stack.top();
}
bool iso_archive::is_valid() const
{
return !m_root.metadata.name.empty();
}
bool iso_archive::exists(const std::string& path)
{
return retrieve(path) != nullptr;
}
bool iso_archive::is_file(const std::string& path)
{
const auto file_node = retrieve(path);
if (!file_node)
{
return false;
}
return !file_node->metadata.is_directory;
}
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)
{
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)
{
const auto node = retrieve(path);
if (!node)
{
return psf::registry();
}
const fs::file psf_file(get_iso_file(m_path, fs::read, *node));
return psf::load_object(psf_file, path);
}
iso_file::iso_file(const std::string& path, bs_t<fs::open_mode> mode)
{
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()
{
return fs::stat_t
{
.is_directory = false,
.is_symlink = false,
.is_writable = false,
.size = size(),
.atime = m_meta.time,
.mtime = m_meta.time,
.ctime = m_meta.time
};
}
bool iso_file::trunc(u64 /*length*/)
{
fs::g_tls_error = fs::error::readonly;
return false;
}
std::pair<u64, iso_extent_info> iso_file::get_extent_pos(u64 pos) const
{
ensure(!m_meta.extents.empty());
auto it = m_meta.extents.begin();
while (pos >= it->size && it != m_meta.extents.end() - 1)
{
pos -= it->size;
it++;
}
return {pos, *it};
}
u64 iso_file::local_extent_remaining(u64 pos) const
{
const auto [local_pos, extent] = get_extent_pos(pos);
return extent.size - local_pos;
}
u64 iso_file::local_extent_size(u64 pos) const
{
return get_extent_pos(pos).second.size;
}
// Assumed valid and in bounds
u64 iso_file::file_offset(u64 pos) const
{
const auto [local_pos, extent] = get_extent_pos(pos);
return (extent.start * ISO_SECTOR_SIZE) + local_pos;
}
u64 iso_file::read(void* buffer, u64 size)
{
const auto r = read_at(m_pos, buffer, size);
m_pos += r;
return r;
}
u64 iso_file::read_at(u64 offset, void* buffer, u64 size)
{
u64 max_size = std::min(size, local_extent_remaining(offset));
if (max_size == 0)
{
return 0;
}
const u64 total_size = this->size();
const u64 archive_first_offset = file_offset(offset);
// If it's not a raw device
if (!m_raw_device)
{
u64 total_read = m_file.read_at(archive_first_offset, buffer, max_size);
if (total_read != max_size)
{
iso_log.error("read_at: %s: Error reading from file - O: %llu (%llu), S: %llu/%llu (%llu), TR: %llu", m_meta.name,
offset, archive_first_offset, max_size, size, total_size, total_read);
return 0;
}
// If present, read the remaining chunk of data on next extent
if (size > max_size && (offset + max_size) < total_size)
{
iso_log.warning("read_at: %s: Extent limit reached reading from file (%llu/%llu)", m_meta.name, max_size, size);
max_size += read_at(offset + max_size, &reinterpret_cast<u8*>(buffer)[max_size], size - max_size);
}
return max_size;
}
// If it's a raw device
// IMPORTANT NOTE:
//
// 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: | ' ' |
// ----------------------------------------------------------------------
// ' '
// ---------------------------------------------
// buffer: | |
// ---------------------------------------------
// ' ' ' '
// -------------------------------------------------------------------------------------------------------------------------------------
// ISO archive: | sec 0 | sec 1 |xxxxx######'###########'###########'###########'##xxxxxxxxx| | ... | sec n-1 | sec n |
// -------------------------------------------------------------------------------------------------------------------------------------
// ' ' ' '
// | first sec | inner sec(s) | last sec |
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 total_read = m_file.read_at(first_sec.lba_address, aligned_buf, ISO_SECTOR_SIZE);
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 != ISO_SECTOR_SIZE)
{
iso_log.error("read_at: %s: Error reading from file - O: %llu (%llu), S: %llu/%llu/%llu (%llu), TR: %llu", m_meta.name,
offset, first_sec.lba_address, ISO_SECTOR_SIZE, max_size, size, total_size, total_read);
return 0;
}
// If present, read the remaining chunk of data on next extent
if (size > max_size && (offset + max_size) < total_size)
{
iso_log.warning("read_at: %s: Extent limit reached reading from file (%llu/%llu)", m_meta.name, max_size, size);
max_size += read_at(offset + max_size, &reinterpret_cast<u8*>(buffer)[max_size], size - max_size);
}
return max_size;
}
//
// Inner sector(s), if any
//
if (sector_count > 2) // If inner sector(s) are present
{
u64 sector_offset = 0;
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);
std::memcpy(&reinterpret_cast<u8*>(buffer)[first_sec.size + sector_offset], aligned_buf, ISO_SECTOR_SIZE);
}
}
//
// Last sector
//
total_read += m_file.read_at(last_sec.address_aligned, aligned_buf, ISO_SECTOR_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
//
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 - O: %llu (%llu), S: %llu/%llu/%llu (%llu), TR: %llu/%llu", m_meta.name,
offset, first_sec.lba_address, ISO_SECTOR_SIZE, max_size, size, total_size,
total_read, ISO_SECTOR_SIZE + ISO_SECTOR_SIZE + (sector_count - 2) * ISO_SECTOR_SIZE);
return 0;
}
// If present, read the remaining chunk of data on next extent
if (size > max_size && (offset + max_size) < total_size)
{
iso_log.warning("read_at: %s: Extent limit reached reading from file (%llu/%llu)", m_meta.name, max_size, size);
max_size += read_at(offset + max_size, &reinterpret_cast<u8*>(buffer)[max_size], size - max_size);
}
return max_size;
}
u64 iso_file::write(const void* /*buffer*/, u64 /*size*/)
{
fs::g_tls_error = fs::error::readonly;
return 0;
}
u64 iso_file::seek(s64 offset, fs::seek_mode whence)
{
const s64 new_pos =
whence == fs::seek_set ? offset :
whence == fs::seek_cur ? offset + m_pos :
whence == fs::seek_end ? offset + size() : -1;
if (new_pos < 0)
{
fs::g_tls_error = fs::error::inval;
return -1;
}
if (m_file.seek(file_offset(new_pos)) == umax)
{
return umax;
}
m_pos = new_pos;
return m_pos;
}
u64 iso_file::size()
{
u64 extent_sizes = 0;
for (const auto& extent : m_meta.extents)
{
extent_sizes += extent.size;
}
return extent_sizes;
}
void iso_file::release()
{
m_file.release();
}
bool iso_dir::read(fs::dir_entry& entry)
{
if (m_pos < m_node.children.size())
{
const auto& selected = m_node.children[m_pos].get()->metadata;
entry.name = selected.name;
entry.atime = selected.time;
entry.mtime = selected.time;
entry.ctime = selected.time;
entry.is_directory = selected.is_directory;
entry.is_symlink = false;
entry.is_writable = false;
entry.size = selected.size();
m_pos++;
return true;
}
return false;
}
void iso_dir::rewind()
{
m_pos = 0;
}
bool iso_device::stat(const std::string& path, fs::stat_t& info)
{
const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string();
const auto node = m_archive.retrieve(relative_path);
if (!node)
{
fs::g_tls_error = fs::error::noent;
return false;
}
const auto& meta = node->metadata;
info = fs::stat_t
{
.is_directory = meta.is_directory,
.is_symlink = false,
.is_writable = false,
.size = meta.size(),
.atime = meta.time,
.mtime = meta.time,
.ctime = meta.time
};
return true;
}
bool iso_device::statfs(const std::string& path, fs::device_stat& info)
{
const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string();
const auto node = m_archive.retrieve(relative_path);
if (!node)
{
fs::g_tls_error = fs::error::noent;
return false;
}
const u64 size = node->metadata.size();
info = fs::device_stat
{
.block_size = size,
.total_size = size,
.total_free = 0,
.avail_free = 0
};
return true;
}
std::unique_ptr<fs::file_base> iso_device::open(const std::string& path, bs_t<fs::open_mode> mode)
{
const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string();
const auto node = m_archive.retrieve(relative_path);
if (!node)
{
fs::g_tls_error = fs::error::noent;
return nullptr;
}
if (node->metadata.is_directory)
{
fs::g_tls_error = fs::error::isdir;
return nullptr;
}
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)
{
const auto relative_path = std::filesystem::relative(std::filesystem::path(path), std::filesystem::path(fs_prefix)).string();
const auto node = m_archive.retrieve(relative_path);
if (!node)
{
fs::g_tls_error = fs::error::noent;
return nullptr;
}
if (!node->metadata.is_directory)
{
// fs::dir::open -> ::readdir should return ENOTDIR when path is pointing to a file instead of a folder.
fs::g_tls_error = fs::error::notdir;
return nullptr;
}
return std::make_unique<iso_dir>(*node);
}
void load_iso(const std::string& path)
{
sys_log.notice("Loading ISO '%s'", path);
fs::set_virtual_device("iso_overlay_fs_dev", stx::make_shared<iso_device>(path));
vfs::mount("/dev_bdvd/"sv, iso_device::virtual_device_name + "/");
}
void unload_iso()
{
sys_log.notice("Unloading ISO");
fs::set_virtual_device("iso_overlay_fs_dev", stx::shared_ptr<iso_device>());
}