split rpcs3 and hle libraries

merge rpcs3 utilities
This commit is contained in:
DH 2025-04-08 19:46:57 +03:00
parent b33e2662b6
commit 62ad27d1e2
1233 changed files with 7004 additions and 3819 deletions

31
rpcs3/Loader/ELF.cpp Normal file
View file

@ -0,0 +1,31 @@
#include "stdafx.h"
#include "ELF.h"
// ELF loading error information
template <>
void fmt_class_string<elf_error>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](elf_error error)
{
switch (error)
{
case elf_error::ok: return "OK";
case elf_error::stream: return "File not found";
case elf_error::stream_header: return "Failed to read ELF header";
case elf_error::stream_phdrs: return "Failed to read ELF program headers";
case elf_error::stream_shdrs: return "Failed to read ELF section headers";
case elf_error::stream_data: return "Failed to read ELF program data";
case elf_error::header_magic: return "Not an ELF";
case elf_error::header_version: return "Invalid or unsupported ELF format";
case elf_error::header_class: return "Invalid ELF class";
case elf_error::header_machine: return "Invalid ELF machine";
case elf_error::header_endianness: return "Invalid ELF data (endianness)";
case elf_error::header_type: return "Invalid ELF type";
case elf_error::header_os: return "Invalid ELF OS ABI";
}
return unknown;
});
}

550
rpcs3/Loader/ELF.h Normal file
View file

@ -0,0 +1,550 @@
#pragma once
#include "util/types.hpp"
#include "util/File.h"
#include "util/bit_set.h"
#include <span>
enum class elf_os : u8
{
none = 0,
lv2 = 0x66,
};
enum class elf_type : u16
{
none = 0,
rel = 1,
exec = 2,
dyn = 3,
core = 4,
prx = 0xffa4,
psv1 = 0xfe00, // ET_SCE_EXEC
psv2 = 0xfe04, // ET_SCE_RELEXEC (vitasdk)
};
enum class elf_machine : u16
{
ppc64 = 0x15,
spu = 0x17,
arm = 0x28,
mips = 0x08,
};
enum class sec_type : u32
{
sht_null = 0,
sht_progbits = 1,
sht_symtab = 2,
sht_strtab = 3,
sht_rela = 4,
sht_hash = 5,
sht_dynamic = 6,
sht_note = 7,
sht_nobits = 8,
sht_rel = 9,
};
enum class sh_flag : u32
{
shf_write,
shf_alloc,
shf_execinstr,
__bitset_enum_max
};
constexpr bool is_memorizable_section(sec_type type, bs_t<sh_flag> flags)
{
switch (type)
{
case sec_type::sht_null:
case sec_type::sht_nobits:
{
return false;
}
case sec_type::sht_progbits:
{
return flags.all_of(sh_flag::shf_alloc);
}
default:
{
if (type > sec_type::sht_rel)
{
return false;
}
return true;
}
}
}
template <typename T>
using elf_be = be_t<T>;
template <typename T>
using elf_le = le_t<T>;
template <template <typename T> class en_t, typename sz_t>
struct elf_ehdr
{
nse_t<u32> e_magic;
u8 e_class;
u8 e_data;
u8 e_curver;
elf_os e_os_abi;
u8 e_abi_ver;
u8 e_pad[7];
en_t<elf_type> e_type;
en_t<elf_machine> e_machine;
en_t<u32> e_version;
en_t<sz_t> e_entry;
en_t<sz_t> e_phoff;
en_t<sz_t> e_shoff;
en_t<u32> e_flags;
en_t<u16> e_ehsize;
en_t<u16> e_phentsize;
en_t<u16> e_phnum;
en_t<u16> e_shentsize;
en_t<u16> e_shnum;
en_t<u16> e_shstrndx;
};
template <template <typename T> class en_t, typename sz_t>
struct elf_phdr
{
static_assert(!sizeof(sz_t), "Invalid elf size type (must be u32 or u64)");
};
template <template <typename T> class en_t>
struct elf_phdr<en_t, u64>
{
en_t<u32> p_type;
en_t<u32> p_flags;
en_t<u64> p_offset;
en_t<u64> p_vaddr;
en_t<u64> p_paddr;
en_t<u64> p_filesz;
en_t<u64> p_memsz;
en_t<u64> p_align;
};
template <template <typename T> class en_t>
struct elf_phdr<en_t, u32>
{
en_t<u32> p_type;
en_t<u32> p_offset;
en_t<u32> p_vaddr;
en_t<u32> p_paddr;
en_t<u32> p_filesz;
en_t<u32> p_memsz;
en_t<u32> p_flags;
en_t<u32> p_align;
};
template <template <typename T> class en_t, typename sz_t>
struct elf_prog final : elf_phdr<en_t, sz_t>
{
std::vector<uchar> bin{};
using base = elf_phdr<en_t, sz_t>;
elf_prog() = default;
elf_prog(u32 type, u32 flags, sz_t vaddr, sz_t memsz, sz_t align, std::vector<uchar>&& bin)
: bin(std::move(bin))
{
base::p_type = type;
base::p_flags = flags;
base::p_vaddr = vaddr;
base::p_memsz = memsz;
base::p_align = align;
base::p_filesz = static_cast<sz_t>(this->bin.size());
base::p_paddr = 0;
base::p_offset = -1;
}
};
template <template <typename T> class en_t, typename sz_t>
struct elf_shdr
{
en_t<u32> sh_name;
en_t<sec_type> sh_type;
en_t<sz_t> _sh_flags;
en_t<sz_t> sh_addr;
en_t<sz_t> sh_offset;
en_t<sz_t> sh_size;
en_t<u32> sh_link;
en_t<u32> sh_info;
en_t<sz_t> sh_addralign;
en_t<sz_t> sh_entsize;
bs_t<sh_flag> sh_flags() const
{
return std::bit_cast<bs_t<sh_flag>>(static_cast<u32>(+_sh_flags));
}
};
template <template <typename T> class en_t, typename sz_t>
struct elf_shdata final : elf_shdr<en_t, sz_t>
{
std::vector<uchar> bin{};
std::span<const uchar> bin_view{};
using base = elf_shdr<en_t, sz_t>;
elf_shdata() = default;
std::span<const uchar> get_bin() const
{
if (!bin_view.empty())
{
return bin_view;
}
return {bin.data(), bin.size()};
}
};
// ELF loading options
enum class elf_opt : u32
{
no_programs, // Don't load phdrs, implies no_data
no_sections, // Don't load shdrs
no_data, // Load phdrs without data
__bitset_enum_max
};
// ELF loading error
enum class elf_error
{
ok = 0,
stream,
stream_header,
stream_phdrs,
stream_shdrs,
stream_data,
header_magic,
header_version,
header_class,
header_machine,
header_endianness,
header_type,
header_os,
};
// ELF object with specified parameters.
// en_t: endianness (elf_le or elf_be)
// sz_t: size (u32 for ELF32, u64 for ELF64)
template <template <typename T> class en_t, typename sz_t, elf_machine Machine, elf_os OS, elf_type Type>
class elf_object
{
elf_error m_error = elf_error::stream; // Set initial error to "file not found" error
public:
using ehdr_t = elf_ehdr<en_t, sz_t>;
using phdr_t = elf_phdr<en_t, sz_t>;
using shdr_t = elf_shdr<en_t, sz_t>;
using prog_t = elf_prog<en_t, sz_t>;
using shdata_t = elf_shdata<en_t, sz_t>;
ehdr_t header{};
std::vector<prog_t> progs{};
std::vector<shdata_t> shdrs{};
usz highest_offset = 0;
public:
elf_object() = default;
elf_object(const fs::file& stream, u64 offset = 0, bs_t<elf_opt> opts = {})
{
open(stream, offset, opts);
}
elf_error open(const fs::file& stream, u64 offset = 0, bs_t<elf_opt> opts = {})
{
highest_offset = 0;
// Check stream
if (!stream)
return set_error(elf_error::stream);
// Read ELF header
highest_offset = sizeof(header);
if (sizeof(header) != stream.read_at(offset, &header, sizeof(header)))
return set_error(elf_error::stream_header);
// Check magic
if (header.e_magic != "\177ELF"_u32)
return set_error(elf_error::header_magic);
// Check class
if (header.e_class != (std::is_same_v<sz_t, u32> ? 1 : 2))
return set_error(elf_error::header_class);
// Check endianness
if (header.e_data != (std::is_same_v<en_t<u32>, le_t<u32>> ? 1 : 2))
return set_error(elf_error::header_endianness);
// Check machine
if (header.e_machine != Machine)
return set_error(elf_error::header_machine);
// Check OS only if specified (hack)
if (OS != elf_os::none && header.e_os_abi != OS)
return set_error(elf_error::header_os);
// Check type only if specified (hack)
if (Type != elf_type::none && header.e_type != Type)
return set_error(elf_error::header_type);
// Check version and other params
if (header.e_curver != 1 || header.e_version != 1u || header.e_ehsize != u16{sizeof(ehdr_t)})
return set_error(elf_error::header_version);
if (header.e_phnum && header.e_phentsize != u16{sizeof(phdr_t)})
return set_error(elf_error::header_version);
if (header.e_shnum && header.e_shentsize != u16{sizeof(shdr_t)})
return set_error(elf_error::header_version);
// Load program headers
std::vector<phdr_t> _phdrs;
std::vector<shdr_t> _shdrs;
u64 seek_pos = 0;
if (!(opts & elf_opt::no_programs))
{
seek_pos = offset + header.e_phoff;
highest_offset = std::max<usz>(highest_offset, seek_pos);
if (!stream.read(_phdrs, header.e_phnum, true, seek_pos))
return set_error(elf_error::stream_phdrs);
}
if (!(opts & elf_opt::no_sections))
{
seek_pos = offset + header.e_shoff;
highest_offset = std::max<usz>(highest_offset, seek_pos);
if (!stream.read(_shdrs, header.e_shnum, true, seek_pos))
return set_error(elf_error::stream_shdrs);
}
progs.clear();
progs.reserve(_phdrs.size());
for (const auto& hdr : _phdrs)
{
static_cast<phdr_t&>(progs.emplace_back()) = hdr;
if (!(opts & elf_opt::no_data))
{
seek_pos = offset + hdr.p_offset;
highest_offset = std::max<usz>(highest_offset, seek_pos);
if (!stream.read(progs.back().bin, hdr.p_filesz, true, seek_pos))
return set_error(elf_error::stream_data);
}
}
shdrs.clear();
shdrs.reserve(_shdrs.size());
for (const auto& shdr : _shdrs)
{
static_cast<shdr_t&>(shdrs.emplace_back()) = shdr;
if (!(opts & elf_opt::no_data) && is_memorizable_section(shdr.sh_type, shdr.sh_flags()))
{
usz p_index = umax;
for (const auto& hdr : _phdrs)
{
// Try to find it in phdr data instead of allocating new section
p_index++;
if (hdr.p_offset <= shdr.sh_offset && shdr.sh_offset + shdr.sh_size - 1 <= hdr.p_offset + hdr.p_filesz - 1)
{
const auto& prog = ::at32(progs, p_index);
shdrs.back().bin_view = {prog.bin.data() + shdr.sh_offset - hdr.p_offset, shdr.sh_size};
}
}
if (!shdrs.back().bin_view.empty())
{
// Optimized
continue;
}
seek_pos = offset + shdr.sh_offset;
highest_offset = std::max<usz>(highest_offset, seek_pos);
if (!stream.read(shdrs.back().bin, shdr.sh_size, true, seek_pos))
return set_error(elf_error::stream_data);
}
}
shdrs.shrink_to_fit();
progs.shrink_to_fit();
return m_error = elf_error::ok;
}
std::vector<u8> save(std::vector<u8>&& init = std::vector<u8>{}) const
{
if (get_error() != elf_error::ok)
{
return std::move(init);
}
fs::file stream = fs::make_stream<std::vector<u8>>(std::move(init));
const bool fixup_shdrs = shdrs.empty() || shdrs[0].sh_type != sec_type::sht_null;
// Write header
ehdr_t header{};
header.e_magic = "\177ELF"_u32;
header.e_class = std::is_same_v<sz_t, u32> ? 1 : 2;
header.e_data = std::is_same_v<en_t<u32>, le_t<u32>> ? 1 : 2;
header.e_curver = 1;
header.e_os_abi = OS != elf_os::none ? OS : this->header.e_os_abi;
header.e_abi_ver = this->header.e_abi_ver;
header.e_type = Type != elf_type::none ? Type : static_cast<elf_type>(this->header.e_type);
header.e_machine = Machine;
header.e_version = 1;
header.e_entry = this->header.e_entry;
header.e_phoff = u32{sizeof(ehdr_t)};
header.e_shoff = u32{sizeof(ehdr_t)} + u32{sizeof(phdr_t)} * ::size32(progs);
header.e_flags = this->header.e_flags;
header.e_ehsize = u32{sizeof(ehdr_t)};
header.e_phentsize = u32{sizeof(phdr_t)};
header.e_phnum = ::size32(progs);
header.e_shentsize = u32{sizeof(shdr_t)};
header.e_shnum = ::size32(shdrs) + u32{fixup_shdrs};
header.e_shstrndx = this->header.e_shstrndx;
stream.write(header);
sz_t off = header.e_shoff + u32{sizeof(shdr_t)} * ::size32(shdrs);
for (phdr_t phdr : progs)
{
phdr.p_offset = std::exchange(off, off + phdr.p_filesz);
stream.write(phdr);
}
if (fixup_shdrs)
{
// Insert a must-have empty section at the start
stream.write(shdr_t{});
}
for (const auto& shdr : shdrs)
{
shdr_t out = static_cast<shdr_t>(shdr);
if (is_memorizable_section(shdr.sh_type, shdr.sh_flags()))
{
usz p_index = umax;
usz data_base = header.e_shoff + u32{sizeof(shdr_t)} * ::size32(shdrs);
bool result = false;
for (const auto& hdr : progs)
{
if (shdr.bin_view.empty())
{
break;
}
// Try to find it in phdr data instead of writing new section
p_index++;
// Rely on previous sh_offset value!
if (hdr.p_offset <= shdr.sh_offset && shdr.sh_offset + shdr.sh_size - 1 <= hdr.p_offset + hdr.p_filesz - 1)
{
out.sh_offset = ::narrow<sz_t>(data_base + static_cast<usz>(shdr.sh_offset - hdr.p_offset));
result = true;
break;
}
data_base += ::at32(progs, p_index).p_filesz;
}
if (result)
{
// Optimized
}
else
{
out.sh_offset = std::exchange(off, off + shdr.sh_size);
}
}
stream.write(static_cast<shdr_t>(out));
}
// Write data
for (const auto& prog : progs)
{
stream.write(prog.bin);
}
for (const auto& shdr : shdrs)
{
if (!is_memorizable_section(shdr.sh_type, shdr.sh_flags()) || !shdr.bin_view.empty())
{
continue;
}
stream.write(shdr.bin);
}
return std::move(static_cast<fs::container_stream<std::vector<u8>>*>(stream.release().get())->obj);
}
elf_object& clear()
{
// Do not use clear() in order to dealloc memory
progs = {};
shdrs = {};
header.e_magic = 0;
m_error = elf_error::stream;
return *this;
}
elf_object& set_error(elf_error error)
{
// Setting an error causes the state to clear if there was no error before
if (m_error == elf_error::ok && error != elf_error::ok)
clear();
m_error = error;
return *this;
}
// Return error code
operator elf_error() const
{
return m_error;
}
elf_error get_error() const
{
return m_error;
}
};
using ppu_exec_object = elf_object<elf_be, u64, elf_machine::ppc64, elf_os::none, elf_type::exec>;
using ppu_prx_object = elf_object<elf_be, u64, elf_machine::ppc64, elf_os::lv2, elf_type::prx>;
using ppu_rel_object = elf_object<elf_be, u64, elf_machine::ppc64, elf_os::lv2, elf_type::rel>;
using spu_exec_object = elf_object<elf_be, u32, elf_machine::spu, elf_os::none, elf_type::exec>;
using spu_rel_object = elf_object<elf_be, u32, elf_machine::spu, elf_os::none, elf_type::rel>;
using arm_exec_object = elf_object<elf_le, u32, elf_machine::arm, elf_os::none, elf_type::none>;

431
rpcs3/Loader/PSF.cpp Normal file
View file

@ -0,0 +1,431 @@
#include "stdafx.h"
#include "PSF.h"
#include "util/asm.hpp"
#include <span>
LOG_CHANNEL(psf_log, "PSF");
template <>
void fmt_class_string<psf::format>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](auto fmt)
{
switch (fmt)
{
STR_CASE(psf::format::array);
STR_CASE(psf::format::string);
STR_CASE(psf::format::integer);
}
return unknown;
});
}
template <>
void fmt_class_string<psf::error>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](auto fmt)
{
switch (fmt)
{
case psf::error::ok: return "OK";
case psf::error::stream: return "File doesn't exist";
case psf::error::not_psf: return "File is not of PSF format";
case psf::error::corrupt: return "PSF is truncated or corrupted";
}
return unknown;
});
}
template <>
void fmt_class_string<psf::registry>::format(std::string& out, u64 arg)
{
const psf::registry& psf = get_object(arg);
for (const auto& entry : psf)
{
if (entry.second.type() == psf::format::array)
{
// Format them last
continue;
}
fmt::append(out, "%s: ", entry.first);
const psf::entry& data = entry.second;
if (data.type() == psf::format::integer)
{
fmt::append(out, "0x%x", data.as_integer());
}
else
{
fmt::append(out, "\"%s\"", data.as_string());
}
out += '\n';
}
for (const auto& entry : psf)
{
if (entry.second.type() != psf::format::array)
{
// Formatted before
continue;
}
fmt::append(out, "%s: %s\n", entry.first, std::span<const u8>(reinterpret_cast<const u8*>(entry.second.as_string().data()), entry.second.size()));
}
}
namespace psf
{
struct header_t
{
le_t<u32> magic;
le_t<u32> version;
le_t<u32> off_key_table;
le_t<u32> off_data_table;
le_t<u32> entries_num;
};
struct def_table_t
{
le_t<u16> key_off;
le_t<format> param_fmt;
le_t<u32> param_len;
le_t<u32> param_max;
le_t<u32> data_off;
};
entry::entry(format type, u32 max_size, std::string_view value, bool allow_truncate) noexcept
: m_type(type), m_max_size(max_size), m_value_string(value)
{
ensure(type == format::string || type == format::array);
ensure(max_size > (type == format::string ? 1u : 0u));
if (allow_truncate && value.size() > max(false))
{
m_value_string.resize(max(false));
}
}
entry::entry(u32 value) noexcept
: m_type(format::integer), m_max_size(sizeof(u32)), m_value_integer(value)
{
}
const std::string& entry::as_string() const
{
ensure(m_type == format::string || m_type == format::array);
return m_value_string;
}
u32 entry::as_integer() const
{
ensure(m_type == format::integer);
return m_value_integer;
}
entry& entry::operator=(std::string_view value)
{
ensure(m_type == format::string || m_type == format::array);
m_value_string = value;
return *this;
}
entry& entry::operator=(u32 value)
{
ensure(m_type == format::integer);
m_value_integer = value;
return *this;
}
u32 entry::size() const
{
switch (m_type)
{
case format::string:
case format::array:
return std::min(m_max_size, ::narrow<u32>(m_value_string.size() + (m_type == format::string ? 1 : 0)));
case format::integer:
return sizeof(u32);
}
fmt::throw_exception("Invalid format (0x%x)", m_type);
}
bool entry::is_valid() const
{
switch (m_type)
{
case format::string:
case format::array:
return m_value_string.size() <= this->max(false);
case format::integer:
return true;
default: break;
}
fmt::throw_exception("Invalid format (0x%x)", m_type);
}
load_result_t load(const fs::file& stream, std::string_view filename)
{
load_result_t result{};
#define PSF_CHECK(cond, err) \
if (!static_cast<bool>(cond)) \
{ \
if (err != error::stream) \
psf_log.error("Error loading PSF '%s': %s%s", filename, err, std::source_location::current()); \
result.sfo.clear(); \
result.errc = err; \
return result; \
}
PSF_CHECK(stream, error::stream);
stream.seek(0);
// Get header
header_t header;
PSF_CHECK(stream.read(header), error::not_psf);
// Check magic and version
PSF_CHECK(header.magic == "\0PSF"_u32, error::not_psf);
PSF_CHECK(header.version == 0x101u, error::not_psf);
PSF_CHECK(header.off_key_table >= sizeof(header_t), error::corrupt);
PSF_CHECK(header.off_key_table <= header.off_data_table, error::corrupt);
PSF_CHECK(header.off_data_table <= stream.size(), error::corrupt);
// Get indices
std::vector<def_table_t> indices;
PSF_CHECK(stream.read(indices, header.entries_num), error::corrupt);
// Get keys
std::string keys;
PSF_CHECK(stream.seek(header.off_key_table) == header.off_key_table, error::corrupt);
PSF_CHECK(stream.read(keys, header.off_data_table - header.off_key_table), error::corrupt);
// Load entries
for (u32 i = 0; i < header.entries_num; ++i)
{
PSF_CHECK(indices[i].key_off < header.off_data_table - header.off_key_table, error::corrupt);
// Get key name (null-terminated string)
std::string key(keys.data() + indices[i].key_off);
// Check entry
PSF_CHECK(!result.sfo.contains(key), error::corrupt);
PSF_CHECK(indices[i].param_len <= indices[i].param_max, error::corrupt);
PSF_CHECK(indices[i].data_off < stream.size() - header.off_data_table, error::corrupt);
PSF_CHECK(indices[i].param_max < stream.size() - indices[i].data_off, error::corrupt);
// Seek data pointer
stream.seek(header.off_data_table + indices[i].data_off);
if (indices[i].param_fmt == format::integer && indices[i].param_max == sizeof(u32) && indices[i].param_len == sizeof(u32))
{
// Integer data
le_t<u32> value;
PSF_CHECK(stream.read(value), error::corrupt);
result.sfo.emplace(std::piecewise_construct,
std::forward_as_tuple(std::move(key)),
std::forward_as_tuple(value));
}
else if (indices[i].param_fmt == format::string || indices[i].param_fmt == format::array)
{
// String/array data
std::string value;
PSF_CHECK(stream.read(value, indices[i].param_len), error::corrupt);
if (indices[i].param_fmt == format::string)
{
// Find null terminator
value.resize(std::strlen(value.c_str()));
}
result.sfo.emplace(std::piecewise_construct,
std::forward_as_tuple(std::move(key)),
std::forward_as_tuple(indices[i].param_fmt, indices[i].param_max, std::move(value)));
}
else
{
// Possibly unsupported format, entry ignored
psf_log.error("Unknown entry format (key='%s', fmt=0x%x, len=0x%x, max=0x%x)", key, indices[i].param_fmt, indices[i].param_len, indices[i].param_max);
}
}
const auto cat = get_string(result.sfo, "CATEGORY", "");
constexpr std::string_view valid_cats[]{"GD", "DG", "HG", "AM", "AP", "AS", "AT", "AV", "BV", "WT", "HM", "CB", "SF", "2P", "2G", "1P", "PP", "MN", "PE", "2D", "SD", "MS"};
if (std::find(std::begin(valid_cats), std::end(valid_cats), cat) == std::end(valid_cats))
{
psf_log.error("Unknown category ('%s')", cat);
PSF_CHECK(false, error::corrupt);
}
#undef PSF_CHECK
return result;
}
load_result_t load(const std::string& filename)
{
return load(fs::file(filename), filename);
}
std::vector<u8> save_object(const psf::registry& psf, std::vector<u8>&& init)
{
fs::file stream = fs::make_stream<std::vector<u8>>(std::move(init));
std::vector<def_table_t> indices;
indices.reserve(psf.size());
// Generate indices and calculate key table length
usz key_offset = 0, data_offset = 0;
for (const auto& entry : psf)
{
def_table_t index{};
index.key_off = ::narrow<u32>(key_offset);
index.param_fmt = entry.second.type();
index.param_len = entry.second.size();
index.param_max = entry.second.max(true);
index.data_off = ::narrow<u32>(data_offset);
// Update offsets:
key_offset += ::narrow<u32>(entry.first.size() + 1); // key size
data_offset += index.param_max;
indices.push_back(index);
}
// Align next section (data) offset
key_offset = utils::align(key_offset, 4);
// Generate header
header_t header{};
header.magic = "\0PSF"_u32;
header.version = 0x101;
header.off_key_table = ::narrow<u32>(sizeof(header_t) + sizeof(def_table_t) * psf.size());
header.off_data_table = ::narrow<u32>(header.off_key_table + key_offset);
header.entries_num = ::narrow<u32>(psf.size());
// Save header and indices
stream.write(header);
stream.write(indices);
// Save key table
for (const auto& entry : psf)
{
stream.write(entry.first);
stream.write('\0');
}
// Skip padding
stream.trunc(stream.seek(header.off_data_table));
// Save data
for (const auto& entry : psf)
{
const auto fmt = entry.second.type();
const u32 max = entry.second.max(true);
if (fmt == format::integer && max == sizeof(u32))
{
const le_t<u32> value = entry.second.as_integer();
stream.write(value);
}
else if (fmt == format::string || fmt == format::array)
{
std::string_view value = entry.second.as_string();
if (!entry.second.is_valid())
{
// TODO: check real limitations of PSF format
psf_log.error("Entry value shrinkage (key='%s', value='%s', size=0x%zx, max=0x%x)", entry.first, value, value.size(), max);
value = value.substr(0, entry.second.max(false));
}
stream.write(value.data(), value.size());
stream.trunc(stream.seek(max - value.size(), fs::seek_cur)); // Skip up to max_size
}
else
{
fmt::throw_exception("Invalid entry format (key='%s', fmt=0x%x)", entry.first, fmt);
}
}
return std::move(static_cast<fs::container_stream<std::vector<u8>>*>(stream.release().get())->obj);
}
std::string_view get_string(const registry& psf, std::string_view key, std::string_view def)
{
const auto found = psf.find(key);
if (found == psf.end() || (found->second.type() != format::string && found->second.type() != format::array))
{
return def;
}
return found->second.as_string();
}
u32 get_integer(const registry& psf, std::string_view key, u32 def)
{
const auto found = psf.find(key);
if (found == psf.end() || found->second.type() != format::integer)
{
return def;
}
return found->second.as_integer();
}
bool check_registry(const registry& psf, std::function<bool(bool ok, const std::string& key, const entry& value)> validate, std::source_location src_loc)
{
bool psf_ok = true;
for (const auto& [key, value] : psf)
{
bool entry_ok = value.is_valid();
if (validate)
{
// Validate against a custom condition as well (forward error)
if (!validate(entry_ok, key, value))
{
entry_ok = false;
}
}
if (!entry_ok)
{
if (value.type() == format::string)
{
psf_log.error("Entry '%s' is invalid: string='%s'.%s", key, value.as_string(), src_loc);
}
else
{
// TODO: Better logging of other types
psf_log.error("Entry %s is invalid.%s", key, value.as_string(), src_loc);
}
}
if (!entry_ok)
{
// Do not break, run over all entries in order to report all errors
psf_ok = false;
}
}
return psf_ok;
}
} // namespace psf

161
rpcs3/Loader/PSF.h Normal file
View file

@ -0,0 +1,161 @@
#pragma once
#include "util/types.hpp"
#include <map>
#include <string>
#include <string_view>
namespace fs
{
class file;
}
namespace psf
{
enum sound_format_flag : s32
{
lpcm_2 = 1 << 0, // Linear PCM 2 Ch.
lpcm_5_1 = 1 << 2, // Linear PCM 5.1 Ch.
lpcm_7_1 = 1 << 4, // Linear PCM 7.1 Ch.
ac3 = 1 << 8, // Dolby Digital 5.1 Ch.
dts = 1 << 9, // DTS 5.1 Ch.
};
enum resolution_flag : s32
{
_480 = 1 << 0,
_576 = 1 << 1,
_720 = 1 << 2,
_1080 = 1 << 3,
_480_16_9 = 1 << 4,
_576_16_9 = 1 << 5,
};
enum class format : u16
{
array = 0x0004, // claimed to be a non-NTS string (char array)
string = 0x0204,
integer = 0x0404,
};
enum class error
{
ok,
stream,
not_psf,
corrupt,
};
class entry final
{
format m_type{};
u32 m_max_size{}; // Entry max size (supplementary info, stored in PSF format)
u32 m_value_integer{}; // TODO: is it really unsigned?
std::string m_value_string{};
public:
// Construct string entry, assign the value
entry(format type, u32 max_size, std::string_view value, bool allow_truncate = false) noexcept;
// Construct integer entry, assign the value
entry(u32 value) noexcept;
~entry() = default;
const std::string& as_string() const;
u32 as_integer() const;
entry& operator=(std::string_view value);
entry& operator=(u32 value);
format type() const
{
return m_type;
}
u32 max(bool with_nts) const
{
return m_max_size - (!with_nts && m_type == format::string ? 1 : 0);
}
u32 size() const;
bool is_valid() const;
};
// Define PSF registry as a sorted map of entries:
using registry = std::map<std::string, entry, std::less<>>;
struct load_result_t
{
registry sfo;
error errc;
explicit operator bool() const
{
return !sfo.empty();
}
};
// Load PSF registry from SFO binary format
load_result_t load(const fs::file&, std::string_view filename);
load_result_t load(const std::string& filename);
inline registry load_object(const fs::file& f, std::string_view filename)
{
return load(f, filename).sfo;
}
inline registry load_object(const std::string& filename)
{
return load(filename).sfo;
}
// Convert PSF registry to SFO binary format
std::vector<u8> save_object(const registry&, std::vector<u8>&& init = std::vector<u8>{});
// Get string value or default value
std::string_view get_string(const registry& psf, std::string_view key, std::string_view def = ""sv);
// Get integer value or default value
u32 get_integer(const registry& psf, std::string_view key, u32 def = 0);
bool check_registry(const registry& psf, std::function<bool(bool ok, const std::string& key, const entry& value)> validate = {}, std::source_location src_loc = std::source_location::current());
// Assign new entry
inline void assign(registry& psf, std::string_view key, entry&& _entry)
{
const auto found = psf.find(key);
if (found == psf.end())
{
psf.emplace(key, std::move(_entry));
return;
}
found->second = std::move(_entry);
return;
}
// Make string entry
inline entry string(u32 max_size, std::string_view value, bool allow_truncate = false)
{
return {format::string, max_size, value, allow_truncate};
}
// Make string entry (from char[N])
template <usz CharN>
inline entry string(u32 max_size, char (&value_array)[CharN], bool allow_truncate = false)
{
std::string_view value{value_array, CharN};
value = value.substr(0, std::min<usz>(value.find_first_of('\0'), value.size()));
return string(max_size, value, allow_truncate);
}
// Make array entry
inline entry array(u32 max_size, std::string_view value)
{
return {format::array, max_size, value};
}
// Checks if of HDD catgeory (assumes a valid category is being passed)
constexpr bool is_cat_hdd(std::string_view cat)
{
return cat.size() == 2u && cat[1] != 'D' && cat != "DG"sv && cat != "MS"sv;
}
} // namespace psf

116
rpcs3/Loader/PUP.cpp Normal file
View file

@ -0,0 +1,116 @@
#include "stdafx.h"
#include "Crypto/sha1.h"
#include "Crypto/key_vault.h"
#include "PUP.h"
pup_object::pup_object(fs::file&& file) : m_file(std::move(file))
{
if (!m_file)
{
m_error = pup_error::stream;
return;
}
m_file.seek(0);
PUPHeader m_header{};
const usz file_size = m_file.size();
if (!m_file.read(m_header))
{
// File is not large enough to contain header or magic is invalid
m_error = pup_error::header_read;
m_formatted_error = fmt::format("Too small PUP file to contain header: 0x%x", file_size);
return;
}
if (m_header.magic != "SCEUF\0\0\0"_u64)
{
m_error = pup_error::header_magic;
return;
}
// Check if file size is the expected size, use subtraction to avoid overflows
if (file_size < m_header.header_length || file_size - m_header.header_length < m_header.data_length)
{
m_formatted_error = fmt::format("Firmware size mismatch, expected: 0x%x + 0x%x, actual: 0x%x", m_header.header_length, m_header.data_length, file_size);
m_error = pup_error::expected_size;
return;
}
if (!m_header.file_count)
{
m_error = pup_error::header_file_count;
return;
}
if (!m_file.read(m_file_tbl, m_header.file_count) || !m_file.read(m_hash_tbl, m_header.file_count))
{
m_error = pup_error::header_file_count;
return;
}
if (pup_error err = validate_hashes(); err != pup_error::ok)
{
m_error = err;
return;
}
m_error = pup_error::ok;
}
fs::file pup_object::get_file(u64 entry_id) const
{
if (m_error != pup_error::ok)
return {};
for (const PUPFileEntry& file_entry : m_file_tbl)
{
if (file_entry.entry_id == entry_id)
{
std::vector<u8> file_buf(file_entry.data_length);
m_file.seek(file_entry.data_offset);
m_file.read(file_buf, file_entry.data_length);
return fs::make_stream(std::move(file_buf));
}
}
return {};
}
pup_error pup_object::validate_hashes()
{
AUDIT(m_error == pup_error::ok);
std::vector<u8> buffer;
const usz size = m_file.size();
for (const PUPFileEntry& file : m_file_tbl)
{
// Sanity check for offset and length, use subtraction to avoid overflows
if (size < file.data_offset || size - file.data_offset < file.data_length)
{
m_formatted_error = fmt::format("File database entry is invalid. (offset=0x%x, length=0x%x, PUP.size=0x%x)", file.data_offset, file.data_length, size);
return pup_error::file_entries;
}
// Reuse buffer
buffer.resize(file.data_length);
m_file.seek(file.data_offset);
m_file.read(buffer.data(), file.data_length);
u8 output[20] = {};
sha1_hmac(PUP_KEY, sizeof(PUP_KEY), buffer.data(), buffer.size(), output);
// Compare to hash entry
if (std::memcmp(output, m_hash_tbl[&file - m_file_tbl.data()].hash, 20) != 0)
{
return pup_error::hash_mismatch;
}
}
return pup_error::ok;
}

77
rpcs3/Loader/PUP.h Normal file
View file

@ -0,0 +1,77 @@
#pragma once
#include "util/types.hpp"
#include "util/endian.hpp"
#include "util/File.h"
#include <vector>
struct PUPHeader
{
le_t<u64> magic;
be_t<u64> package_version;
be_t<u64> image_version;
be_t<u64> file_count;
be_t<u64> header_length;
be_t<u64> data_length;
};
struct PUPFileEntry
{
be_t<u64> entry_id;
be_t<u64> data_offset;
be_t<u64> data_length;
u8 padding[8];
};
struct PUPHashEntry
{
be_t<u64> entry_id;
u8 hash[20];
u8 padding[4];
};
// PUP loading error
enum class pup_error : u32
{
ok,
stream,
header_read,
header_magic,
header_file_count,
expected_size,
file_entries,
hash_mismatch,
};
class pup_object
{
fs::file m_file{};
pup_error m_error{};
std::string m_formatted_error{};
std::vector<PUPFileEntry> m_file_tbl{};
std::vector<PUPHashEntry> m_hash_tbl{};
pup_error validate_hashes();
public:
pup_object(fs::file&& file);
fs::file& file()
{
return m_file;
}
explicit operator pup_error() const
{
return m_error;
}
const std::string& get_formatted_error() const
{
return m_formatted_error;
}
fs::file get_file(u64 entry_id) const;
};

703
rpcs3/Loader/TAR.cpp Normal file
View file

@ -0,0 +1,703 @@
#include "stdafx.h"
#include "Emu/VFS.h"
#include "Emu/System.h"
#include "Crypto/unself.h"
#include "TAR.h"
#include "util/asm.hpp"
#include "util/serialization_ext.hpp"
#include <charconv>
#include <span>
LOG_CHANNEL(tar_log, "TAR");
fs::file make_file_view(const fs::file& file, u64 offset, u64 size);
// File constructor
tar_object::tar_object(const fs::file& file)
: m_file(std::addressof(file)), m_ar(nullptr), m_ar_tar_start(umax)
{
ensure(*m_file);
}
// Stream (pipe-like) constructor
tar_object::tar_object(utils::serial& ar)
: m_file(nullptr), m_ar(std::addressof(ar)), m_ar_tar_start(ar.pos)
{
}
TARHeader tar_object::read_header(u64 offset) const
{
TARHeader header{};
if (m_ar)
{
ensure(m_ar->pos == m_ar_tar_start + offset);
m_ar->serialize(header);
return header;
}
if (m_file->read_at(offset, &header, sizeof(header)) != sizeof(header))
{
std::memset(&header, 0, sizeof(header));
}
return header;
}
u64 octal_text_to_u64(std::string_view sv)
{
u64 i = umax;
const auto ptr = std::from_chars(sv.data(), sv.data() + sv.size(), i, 8).ptr;
// Range must be terminated with either NUL or space
if (ptr == sv.data() + sv.size() || (*ptr && *ptr != ' '))
{
i = umax;
}
return i;
}
std::vector<std::string> tar_object::get_filenames()
{
std::vector<std::string> vec;
get_file("");
for (auto it = m_map.cbegin(); it != m_map.cend(); ++it)
{
vec.push_back(it->first);
}
return vec;
}
std::unique_ptr<utils::serial> tar_object::get_file(const std::string& path, std::string* new_file_path)
{
std::unique_ptr<utils::serial> m_out;
auto emplace_single_entry = [&](usz& offset, const usz max_size) -> std::pair<usz, std::string>
{
if (offset >= max_size)
{
return {};
}
TARHeader header = read_header(offset);
offset += 512;
u64 size = umax;
std::string filename;
if (std::memcmp(header.magic, "ustar", 5) == 0)
{
const std::string_view size_sv{header.size, std::size(header.size)};
size = octal_text_to_u64(size_sv);
// Check for overflows and if surpasses file size
if ((header.name[0] || header.prefix[0]) && ~size >= 512 && max_size >= size && max_size - size >= offset)
{
// Cache size in native u64 format
static_assert(sizeof(size) < sizeof(header.size));
std::memcpy(header.size, &size, 8);
std::string_view prefix_name{header.prefix, std::size(header.prefix)};
std::string_view name{header.name, std::size(header.name)};
prefix_name = prefix_name.substr(0, prefix_name.find_first_of('\0'));
name = name.substr(0, name.find_first_of('\0'));
filename += prefix_name;
filename += name;
// Save header and offset
m_map.insert_or_assign(filename, std::make_pair(offset, std::move(header)));
if (new_file_path)
{
*new_file_path = filename;
}
return {size, std::move(filename)};
}
tar_log.error("tar_object::get_file() failed to convert header.size=%s, filesize=0x%x", size_sv, max_size);
}
else
{
tar_log.notice("tar_object::get_file() failed to parse header: offset=0x%x, filesize=0x%x, header_first16=0x%016x", offset, max_size, read_from_ptr<be_t<u128>>(reinterpret_cast<const u8*>(&header)));
}
return {size, {}};
};
if (auto it = m_map.find(path); it != m_map.end())
{
u64 size = 0;
std::memcpy(&size, it->second.second.size, sizeof(size));
if (m_file)
{
m_out = std::make_unique<utils::serial>();
m_out->set_reading_state();
m_out->m_file_handler = make_uncompressed_serialization_file_handler(make_file_view(*m_file, it->second.first, size));
}
else
{
m_out = std::make_unique<utils::serial>();
*m_out = std::move(*m_ar);
m_out->m_max_data = m_ar_tar_start + it->second.first + size;
}
return m_out;
}
else if (m_ar && path.empty())
{
const u64 size = emplace_single_entry(largest_offset, m_ar->get_size(umax) - m_ar_tar_start).first;
// Advance offset to next block
largest_offset += utils::align(size, 512);
}
// Continue scanning from last file entered
else if (m_file)
{
const u64 max_size = m_file->size();
while (largest_offset < max_size)
{
const auto [size, filename] = emplace_single_entry(largest_offset, max_size);
if (size == umax)
{
continue;
}
// Advance offset to next block
largest_offset += utils::align(size, 512);
if (!path.empty() && path == filename)
{
// Path is equal, return handle to the file data
return get_file(path);
}
}
}
return m_out;
}
bool tar_object::extract(const std::string& prefix_path, bool is_vfs)
{
std::vector<u8> filedata_buffer(0x80'0000);
std::span<u8> filedata_span{filedata_buffer.data(), filedata_buffer.size()};
auto iter = m_map.begin();
auto get_next = [&](bool is_first)
{
if (m_ar)
{
ensure(!is_first || m_map.empty()); // Must be empty on first call
std::string name_iter;
get_file("", &name_iter); // Get next entry
return m_map.find(name_iter);
}
else if (is_first)
{
get_file(""); // Scan entries
return m_map.begin();
}
else
{
return std::next(iter);
}
};
for (iter = get_next(true); iter != m_map.end(); iter = get_next(false))
{
const TARHeader& header = iter->second.second;
const std::string& name = iter->first;
// Backwards compatibility measure
const bool should_ignore = name.find(reinterpret_cast<const char*>(u8"")) != umax;
std::string result = name;
if (!prefix_path.empty())
{
result = prefix_path + '/' + result;
}
else
{
// Must be VFS here
is_vfs = true;
result.insert(result.begin(), '/');
}
if (is_vfs)
{
result = vfs::get(result);
if (result.empty())
{
tar_log.error("Path of entry is not mounted: '%s' (prefix_path='%s')", name, prefix_path);
return false;
}
}
u64 mtime = octal_text_to_u64({header.mtime, std::size(header.mtime)});
// Let's use it for optional atime
u64 atime = octal_text_to_u64({header.padding, 12});
// This is a fake timestamp, it can be invalid
if (atime == umax)
{
// Set to mtime if not provided
atime = mtime;
}
switch (header.filetype)
{
case '\0':
case '0':
{
// Create the directories which should have been mount points if prefix_path is not empty
if (!prefix_path.empty() && !fs::create_path(fs::get_parent_dir(result)))
{
tar_log.error("TAR Loader: failed to create directory for file %s (%s)", name, fs::g_tls_error);
return false;
}
// For restoring m_ar->m_max_data
usz restore_limit = umax;
if (!m_file)
{
// Restore m_ar (remove limit)
restore_limit = m_ar->m_max_data;
}
std::unique_ptr<utils::serial> file_data = get_file(name);
fs::file file;
if (should_ignore)
{
file = fs::make_stream<std::vector<u8>>();
}
else
{
file.open(result, fs::rewrite);
}
if (file && file_data)
{
while (true)
{
const usz unread_size = file_data->try_read(filedata_span);
if (unread_size == 0)
{
file.write(filedata_span.data(), should_ignore ? 0 : filedata_span.size());
continue;
}
// Tail data
if (usz read_size = filedata_span.size() - unread_size)
{
ensure(file_data->try_read(filedata_span.first(read_size)) == 0);
file.write(filedata_span.data(), should_ignore ? 0 : read_size);
}
break;
}
file.close();
file_data->seek_pos(m_ar_tar_start + largest_offset, true);
if (!m_file)
{
// Restore m_ar
*m_ar = std::move(*file_data);
m_ar->m_max_data = restore_limit;
}
if (should_ignore)
{
break;
}
if (mtime != umax && !fs::utime(result, atime, mtime))
{
tar_log.error("TAR Loader: fs::utime failed on %s (%s)", result, fs::g_tls_error);
return false;
}
tar_log.notice("TAR Loader: written file %s", name);
break;
}
if (!m_file)
{
// Restore m_ar
*m_ar = std::move(*file_data);
m_ar->m_max_data = restore_limit;
}
const auto old_error = fs::g_tls_error;
tar_log.error("TAR Loader: failed to write file %s (%s) (fs::exists=%s)", name, old_error, fs::exists(result));
return false;
}
case '5':
{
if (should_ignore)
{
break;
}
if (!fs::create_path(result))
{
tar_log.error("TAR Loader: failed to create directory %s (%s)", name, fs::g_tls_error);
return false;
}
if (mtime != umax && !fs::utime(result, atime, mtime))
{
tar_log.error("TAR Loader: fs::utime failed on %s (%s)", result, fs::g_tls_error);
return false;
}
break;
}
default:
tar_log.error("TAR Loader: unknown file type: 0x%x", header.filetype);
return false;
}
}
return true;
}
void tar_object::save_directory(const std::string& target_path, utils::serial& ar, const process_func& func, std::vector<fs::dir_entry>&& entries, bool has_evaluated_results, usz src_dir_pos)
{
const bool is_null = ar.m_file_handler && ar.m_file_handler->is_null();
const bool reuse_entries = !is_null || has_evaluated_results;
if (reuse_entries)
{
ensure(!entries.empty());
}
auto write_octal = [](char* ptr, u64 i)
{
if (!i)
{
*ptr = '0';
return;
}
ptr += utils::aligned_div(static_cast<u32>(std::bit_width(i)), 3) - 1;
for (; i; ptr--, i /= 8)
{
*ptr = static_cast<char>('0' + (i % 8));
}
};
auto save_file = [&](const fs::stat_t& file_stat, const std::string& file_name)
{
if (!file_stat.size)
{
return;
}
if (is_null && !func)
{
ar.pos += utils::align(file_stat.size, 512);
return;
}
if (fs::file fd{file_name})
{
const u64 old_pos = ar.pos;
const usz old_size = ar.data.size();
if (func)
{
std::string saved_path{&::at32(file_name, src_dir_pos), file_name.size() - src_dir_pos};
// Use custom function for file saving if provided
// Allows for example to compress PNG files as JPEG in the TAR itself
if (!func(fd, saved_path, ar))
{
// Revert (this entry should not be included if func returns false)
if (is_null)
{
ar.pos = old_pos;
return;
}
ar.data.resize(old_size);
ar.seek_end();
return;
}
if (is_null)
{
// Align
ar.pos += utils::align(ar.pos - old_pos, 512);
return;
}
}
else
{
constexpr usz transfer_block_size = 0x100'0000;
for (usz read_index = 0; read_index < file_stat.size; read_index += transfer_block_size)
{
const usz read_size = std::min<usz>(transfer_block_size, file_stat.size - read_index);
// Read file data
const usz buffer_tail = ar.data.size();
ar.data.resize(buffer_tail + read_size);
ensure(fd.read_at(read_index, ar.data.data() + buffer_tail, read_size) == read_size);
// Set position to the end of data, so breathe() would work correctly
ar.seek_end();
// Allow flushing to file if needed
ar.breathe();
}
}
// Align
const usz diff = ar.pos - old_pos;
ar.data.resize(ar.data.size() + utils::align(diff, 512) - diff);
ar.seek_end();
fd.close();
ensure(fs::utime(file_name, file_stat.atime, file_stat.mtime));
}
else
{
ensure(false);
}
};
auto save_header = [&](const fs::stat_t& stat, const std::string& name)
{
static_assert(sizeof(TARHeader) == 512);
std::string_view saved_path{name.size() == src_dir_pos ? name.c_str() : &::at32(name, src_dir_pos), name.size() - src_dir_pos};
if (is_null)
{
ar.pos += sizeof(TARHeader);
return;
}
if (usz pos = saved_path.find_first_not_of(fs::delim); pos != umax)
{
saved_path = saved_path.substr(pos, saved_path.size());
}
else
{
// Target the destination directory, I do not know if this is compliant with TAR format
saved_path = "/"sv;
}
TARHeader header{};
std::memcpy(header.magic, "ustar ", 6);
// Prefer saving to name field as much as we can
// If it doesn't fit, save 100 characters at name and 155 characters preceding to it at max
const u64 prefix_size = std::clamp<usz>(saved_path.size(), 100, 255) - 100;
std::memcpy(header.prefix, saved_path.data(), prefix_size);
const u64 name_size = std::min<usz>(saved_path.size(), 255) - prefix_size;
std::memcpy(header.name, saved_path.data() + prefix_size, name_size);
write_octal(header.size, stat.is_directory ? 0 : stat.size);
write_octal(header.mtime, stat.mtime);
write_octal(header.padding, stat.atime);
header.filetype = stat.is_directory ? '5' : '0';
ar(header);
ar.breathe();
};
fs::stat_t stat{};
if (src_dir_pos == umax)
{
// First call, get source directory string size so it can be cut from entry paths
src_dir_pos = target_path.size();
}
if (has_evaluated_results)
{
// Save from cached data by previous call
for (auto&& entry : entries)
{
ensure(entry.name.starts_with(target_path));
save_header(entry, entry.name);
if (!entry.is_directory)
{
save_file(entry, entry.name);
}
}
}
else
{
if (entries.empty())
{
if (!fs::get_stat(target_path, stat))
{
return;
}
save_header(stat, target_path);
// Optimization: avoid saving to list if this is not an evaluation call
if (is_null)
{
static_cast<fs::stat_t&>(entries.emplace_back()) = stat;
entries.back().name = target_path;
}
}
else
{
stat = entries.back();
save_header(stat, entries.back().name);
}
if (stat.is_directory)
{
bool exists = false;
for (auto&& entry : fs::dir(target_path))
{
exists = true;
if (entry.name.find_first_not_of('.') == umax || entry.name.starts_with(reinterpret_cast<const char*>(u8"")))
{
continue;
}
entry.name = target_path.ends_with('/') ? target_path + entry.name : target_path + '/' + entry.name;
if (!entry.is_directory)
{
save_header(entry, entry.name);
save_file(entry, entry.name);
// TAR is an old format which does not depend on previous data so memory ventilation is trivial here
ar.breathe();
entries.emplace_back(std::move(entry));
}
else
{
if (!is_null)
{
// Optimization: avoid saving to list if this is not an evaluation call
entries.clear();
}
entries.emplace_back(std::move(entry));
save_directory(::as_rvalue(entries.back().name), ar, func, std::move(entries), false, src_dir_pos);
}
}
ensure(exists);
}
else
{
fs::dir_entry entry{};
entry.name = target_path;
static_cast<fs::stat_t&>(entry) = stat;
save_file(entry, entry.name);
}
ar.breathe();
}
}
bool extract_tar(const std::string& file_path, const std::string& dir_path, fs::file file)
{
tar_log.notice("Extracting '%s' to directory '%s'...", file_path, dir_path);
if (!file)
{
file.open(file_path);
}
if (!file)
{
tar_log.error("Error opening file '%s' (%s)", file_path, fs::g_tls_error);
return false;
}
std::vector<fs::file> vec;
if (SCEDecrypter self_dec(file); self_dec.LoadHeaders())
{
// Encrypted file, decrypt
self_dec.LoadMetadata(SCEPKG_ERK, SCEPKG_RIV);
if (!self_dec.DecryptData())
{
tar_log.error("Failed to decrypt TAR.");
return false;
}
vec = self_dec.MakeFile();
if (vec.size() < 3)
{
tar_log.error("Failed to decrypt TAR.");
return false;
}
}
else
{
// Not an encrypted file
tar_log.warning("TAR is not encrypted, it may not be valid for this tool. Encrypted TAR are known to be found in PS3 Firmware files only.");
}
if (!vfs::mount("/tar_extract", dir_path))
{
tar_log.error("Failed to mount '%s'", dir_path);
return false;
}
tar_object tar(vec.empty() ? file : vec[2]);
const bool ok = tar.extract("/tar_extract", true);
if (ok)
{
tar_log.success("Extraction complete!");
}
else
{
tar_log.error("TAR contents are invalid.");
}
// Unmount
Emu.Init();
return ok;
}

63
rpcs3/Loader/TAR.h Normal file
View file

@ -0,0 +1,63 @@
#pragma once
#include "util/types.hpp"
#include <map>
struct TARHeader
{
char name[100];
char dontcare[24];
char size[12];
char mtime[12];
char chksum[8];
char filetype;
char linkname[100];
char magic[6];
char dontcare2[82];
char prefix[155];
char padding[12]; // atime for RPCS3
ENABLE_BITWISE_SERIALIZATION;
};
namespace fs
{
class file;
struct dir_entry;
} // namespace fs
namespace utils
{
struct serial;
}
class tar_object
{
const fs::file* m_file;
utils::serial* m_ar;
const usz m_ar_tar_start;
usz largest_offset = 0; // We store the largest offset so we can continue to scan from there.
std::map<std::string, std::pair<u64, TARHeader>> m_map{}; // Maps path to offset of file data and its header
TARHeader read_header(u64 offset) const;
public:
tar_object(const fs::file& file);
tar_object(utils::serial& ar);
std::vector<std::string> get_filenames();
std::unique_ptr<utils::serial> get_file(const std::string& path, std::string* new_file_path = nullptr);
using process_func = std::function<bool(const fs::file&, std::string&, utils::serial&)>;
// Extract all files in archive to destination (as VFS if is_vfs is true)
// Allow to optionally specify explicit mount point (which may be directory meant for extraction)
bool extract(const std::string& prefix_path = {}, bool is_vfs = false);
static void save_directory(const std::string& src_dir, utils::serial& ar, const process_func& func = {}, std::vector<fs::dir_entry>&& = std::vector<fs::dir_entry>{}, bool has_evaluated_results = false, usz src_dir_pos = umax);
};
bool extract_tar(const std::string& file_path, const std::string& dir_path, fs::file file = {});

394
rpcs3/Loader/TROPUSR.cpp Normal file
View file

@ -0,0 +1,394 @@
#include "stdafx.h"
#include "Emu/VFS.h"
#include "TROPUSR.h"
LOG_CHANNEL(trp_log, "Trophy");
enum : u32
{
TROPUSR_MAGIC = 0x818F54AD
};
std::shared_ptr<rXmlNode> trophy_xml_document::GetRoot()
{
auto trophy_base = rXmlDocument::GetRoot();
if (!trophy_base)
{
return nullptr;
}
if (auto trophy_conf = trophy_base->GetChildren();
trophy_conf && trophy_conf->GetName() == "trophyconf")
{
trophy_base = trophy_conf;
}
else
{
trp_log.error("trophy_xml_document: Root name does not match trophyconf in trophy. Name: %s", trophy_conf ? trophy_conf->GetName() : trophy_base->GetName());
// TODO: return nullptr or is this possible?
}
return trophy_base;
}
TROPUSRLoader::load_result TROPUSRLoader::Load(std::string_view filepath, std::string_view configpath)
{
const std::string path = vfs::get(filepath);
load_result res{};
// Generate TROPUSR.DAT
auto generate = [&]
{
// Reset filesystem error
fs::g_tls_error = fs::error::ok;
// Generate TROPUSR.DAT if not existing
res.success = Generate(filepath, configpath);
if (!res.success)
{
trp_log.error("TROPUSRLoader::Load(): Failed to generate TROPUSR.DAT (path='%s', cfg='%s', %s)", path, configpath, fs::g_tls_error);
}
m_file.close();
return res;
};
if (!m_file.open(path))
{
return generate();
}
if (!LoadHeader() || !LoadTableHeaders() || !LoadTables())
{
// Ignore existing TROPUSR.DAT because it is invalid
m_file.close();
res.discarded_existing = true;
trp_log.error("TROPUSRLoader::Load(): Failed to load existing TROPUSR.DAT, trying to generate new file with empty trophies history! (path='%s')", path);
return generate();
}
m_file.close();
res.success = true;
return res;
}
bool TROPUSRLoader::LoadHeader()
{
if (!m_file)
{
return false;
}
m_file.seek(0);
if (!m_file.read(m_header) || m_header.magic != TROPUSR_MAGIC)
{
return false;
}
return true;
}
bool TROPUSRLoader::LoadTableHeaders()
{
if (!m_file)
{
return false;
}
m_file.seek(0x30);
m_tableHeaders.clear();
if (!m_file.read(m_tableHeaders, m_header.tables_count))
{
return false;
}
return true;
}
bool TROPUSRLoader::LoadTables()
{
if (!m_file)
{
return false;
}
for (const TROPUSRTableHeader& tableHeader : m_tableHeaders)
{
m_file.seek(tableHeader.offset);
if (tableHeader.type == 4u)
{
m_table4.clear();
if (!m_file.read(m_table4, tableHeader.entries_count))
return false;
}
if (tableHeader.type == 6u)
{
m_table6.clear();
if (!m_file.read(m_table6, tableHeader.entries_count))
return false;
}
// TODO: Other tables
}
return true;
}
// TODO: TROPUSRLoader::Save deletes the TROPUSR and creates it again. This is probably very slow.
bool TROPUSRLoader::Save(std::string_view filepath)
{
fs::pending_file temp(vfs::get(filepath));
if (!temp.file)
{
return false;
}
temp.file.write(m_header);
temp.file.write(m_tableHeaders);
temp.file.write(m_table4);
temp.file.write(m_table6);
return temp.commit();
}
bool TROPUSRLoader::Generate(std::string_view filepath, std::string_view configpath)
{
fs::file config(vfs::get(configpath));
if (!config)
{
return false;
}
trophy_xml_document doc{};
pugi::xml_parse_result res = doc.Read(config.to_string());
if (!res)
{
trp_log.error("TROPUSRLoader::Generate: Failed to read file: %s", filepath);
return false;
}
m_table4.clear();
m_table6.clear();
auto trophy_base = doc.GetRoot();
if (!trophy_base)
{
trp_log.error("TROPUSRLoader::Generate: Failed to read file (root is null): %s", filepath);
return false;
}
for (std::shared_ptr<rXmlNode> n = trophy_base->GetChildren(); n; n = n->GetNext())
{
if (n->GetName() == "trophy")
{
const u32 trophy_id = std::atoi(n->GetAttribute("id").c_str());
const u32 trophy_pid = std::atoi(n->GetAttribute("pid").c_str());
u32 trophy_grade;
switch (n->GetAttribute("ttype")[0])
{
case 'B': trophy_grade = trophy_grade::bronze; break;
case 'S': trophy_grade = trophy_grade::silver; break;
case 'G': trophy_grade = trophy_grade::gold; break;
case 'P': trophy_grade = trophy_grade::platinum; break;
default: trophy_grade = trophy_grade::unknown; break;
}
TROPUSREntry4 entry4 = {4, u32{sizeof(TROPUSREntry4)} - 0x10, ::size32(m_table4), 0, trophy_id, trophy_grade, trophy_pid};
TROPUSREntry6 entry6 = {6, u32{sizeof(TROPUSREntry6)} - 0x10, ::size32(m_table6), 0, trophy_id};
m_table4.push_back(entry4);
m_table6.push_back(entry6);
}
}
u64 offset = sizeof(TROPUSRHeader) + 2 * sizeof(TROPUSRTableHeader);
TROPUSRTableHeader table4header = {4, u32{sizeof(TROPUSREntry4)} - 0x10, 1, ::size32(m_table4), offset};
offset += m_table4.size() * sizeof(TROPUSREntry4);
TROPUSRTableHeader table6header = {6, u32{sizeof(TROPUSREntry6)} - 0x10, 1, ::size32(m_table6), offset};
offset += m_table6.size() * sizeof(TROPUSREntry6);
m_tableHeaders.clear();
m_tableHeaders.push_back(table4header);
m_tableHeaders.push_back(table6header);
std::memset(&m_header, 0, sizeof(m_header));
m_header.magic = TROPUSR_MAGIC;
m_header.unk1 = 0x00010000;
m_header.tables_count = ::size32(m_tableHeaders);
m_header.unk2 = 0;
return Save(filepath);
}
u32 TROPUSRLoader::GetTrophiesCount() const
{
return ::size32(m_table6);
}
u32 TROPUSRLoader::GetUnlockedTrophiesCount() const
{
u32 count = 0;
for (const auto& trophy : m_table6)
{
if (trophy.trophy_state)
{
count++;
}
}
return count;
}
u32 TROPUSRLoader::GetUnlockedPlatinumID(u32 trophy_id, const std::string& config_path)
{
constexpr u32 invalid_trophy_id = -1; // SCE_NP_TROPHY_INVALID_TROPHY_ID;
if (trophy_id >= m_table6.size() || trophy_id >= m_table4.size())
{
trp_log.warning("TROPUSRLoader::GetUnlockedPlatinumID: Invalid id=%d", trophy_id);
return invalid_trophy_id;
}
if (m_table6.size() != m_table4.size())
{
trp_log.warning("TROPUSRLoader::GetUnlockedPlatinumID: Table size mismatch: %d vs. %d", m_table6.size(), m_table4.size());
return invalid_trophy_id;
}
// We need to read the trophy info from file here and update it for backwards compatibility.
// TROPUSRLoader::Generate will currently not be called on existing trophy data which might lack the pid.
fs::file config(config_path);
if (!config)
{
return invalid_trophy_id;
}
trophy_xml_document doc{};
pugi::xml_parse_result res = doc.Read(config.to_string());
if (!res)
{
trp_log.error("TROPUSRLoader::GetUnlockedPlatinumID: Failed to read file: %s", config_path);
return invalid_trophy_id;
}
auto trophy_base = doc.GetRoot();
if (!trophy_base)
{
trp_log.error("TROPUSRLoader::GetUnlockedPlatinumID: Failed to read file (root is null): %s", config_path);
return false;
}
const usz trophy_count = m_table4.size();
for (std::shared_ptr<rXmlNode> n = trophy_base->GetChildren(); n; n = n->GetNext())
{
if (n->GetName() == "trophy")
{
const u32 trophy_id = std::atoi(n->GetAttribute("id").c_str());
const u32 trophy_pid = std::atoi(n->GetAttribute("pid").c_str());
// We currently assume that trophies are ordered
if (trophy_id < trophy_count && m_table4[trophy_id].trophy_id == trophy_id)
{
// Update the pid for backwards compatibility
m_table4[trophy_id].trophy_pid = trophy_pid;
}
}
}
// Get this trophy's platinum link id
const u32 pid = m_table4[trophy_id].trophy_pid;
// The platinum trophy has to have a valid id and must still be locked
if (pid == invalid_trophy_id || GetTrophyUnlockState(pid)) // the first check is redundant but I'll keep it to prevent regressions
{
return invalid_trophy_id;
}
// The platinum trophy stays locked if any relevant trophy is still locked
for (usz i = 0; i < trophy_count; i++)
{
if (m_table4[i].trophy_pid == pid && !m_table6[i].trophy_state)
{
return invalid_trophy_id;
}
}
// All relevant trophies for this platinum link id were unlocked
return pid;
}
u32 TROPUSRLoader::GetTrophyGrade(u32 id) const
{
if (id >= m_table4.size())
{
trp_log.warning("TROPUSRLoader::GetTrophyGrade: Invalid id=%d", id);
return trophy_grade::unknown;
}
return m_table4[id].trophy_grade; // Let's assume the trophies are stored ordered
}
u32 TROPUSRLoader::GetTrophyUnlockState(u32 id) const
{
if (id >= m_table6.size())
{
trp_log.warning("TROPUSRLoader::GetTrophyUnlockState: Invalid id=%d", id);
return 0;
}
return m_table6[id].trophy_state; // Let's assume the trophies are stored ordered
}
u64 TROPUSRLoader::GetTrophyTimestamp(u32 id) const
{
if (id >= m_table6.size())
{
trp_log.warning("TROPUSRLoader::GetTrophyTimestamp: Invalid id=%d", id);
return 0;
}
// TODO: What timestamp does sceNpTrophyGetTrophyInfo want, timestamp1 or timestamp2?
return m_table6[id].timestamp2; // Let's assume the trophies are stored ordered
}
bool TROPUSRLoader::UnlockTrophy(u32 id, u64 timestamp1, u64 timestamp2)
{
if (id >= m_table6.size())
{
trp_log.warning("TROPUSRLoader::UnlockTrophy: Invalid id=%d", id);
return false;
}
m_table6[id].trophy_state = 1;
m_table6[id].timestamp1 = timestamp1;
m_table6[id].timestamp2 = timestamp2;
return true;
}
bool TROPUSRLoader::LockTrophy(u32 id)
{
if (id >= m_table6.size())
{
trp_log.warning("TROPUSRLoader::LockTrophy: Invalid id=%d", id);
return false;
}
m_table6[id].trophy_state = 0;
m_table6[id].timestamp1 = 0;
m_table6[id].timestamp2 = 0;
return true;
}

112
rpcs3/Loader/TROPUSR.h Normal file
View file

@ -0,0 +1,112 @@
#pragma once
#include "stdafx.h"
#include "util/rXml.h"
#include "util/File.h"
struct TROPUSRHeader
{
be_t<u32> magic; // 81 8F 54 AD
be_t<u32> unk1;
be_t<u32> tables_count;
be_t<u32> unk2;
char reserved[32];
};
struct TROPUSRTableHeader
{
be_t<u32> type;
be_t<u32> entries_size;
be_t<u32> unk1; // Seems to be 1
be_t<u32> entries_count;
be_t<u64> offset;
be_t<u64> reserved;
};
struct TROPUSREntry4
{
// Entry Header
be_t<u32> entry_type; // Always 0x4
be_t<u32> entry_size; // Always 0x50
be_t<u32> entry_id; // Entry ID
be_t<u32> entry_unk1; // Just zeroes?
// Entry Contents
be_t<u32> trophy_id; // Trophy ID
be_t<u32> trophy_grade; // This seems interesting
be_t<u32> trophy_pid; // (Assuming that this is the platinum link id) FF FF FF FF (-1) = SCE_NP_TROPHY_INVALID_TROPHY_ID
char unk6[68]; // Just zeroes?
};
struct TROPUSREntry6
{
// Entry Header
be_t<u32> entry_type; // Always 6
be_t<u32> entry_size; // Always 0x60
be_t<u32> entry_id; // Entry ID
be_t<u32> entry_unk1; // Just zeroes?
// Entry Contents
be_t<u32> trophy_id; // Trophy ID
be_t<u32> trophy_state; // Wild guess: 00 00 00 00 = Locked, 00 00 00 01 = Unlocked
be_t<u32> unk4; // This seems interesting
be_t<u32> unk5; // Just zeroes?
be_t<u64> timestamp1;
be_t<u64> timestamp2;
char unk6[64]; // Just zeroes?
// Note: One of the fields should hold a flag showing whether the trophy is hidden or not
};
struct trophy_xml_document : public rXmlDocument
{
trophy_xml_document() : rXmlDocument() {}
std::shared_ptr<rXmlNode> GetRoot() override;
};
class TROPUSRLoader
{
enum trophy_grade : u32
{
unknown = 0, // SCE_NP_TROPHY_GRADE_UNKNOWN
platinum = 1, // SCE_NP_TROPHY_GRADE_PLATINUM
gold = 2, // SCE_NP_TROPHY_GRADE_GOLD
silver = 3, // SCE_NP_TROPHY_GRADE_SILVER
bronze = 4 // SCE_NP_TROPHY_GRADE_BRONZE
};
fs::file m_file;
TROPUSRHeader m_header{};
std::vector<TROPUSRTableHeader> m_tableHeaders;
std::vector<TROPUSREntry4> m_table4;
std::vector<TROPUSREntry6> m_table6;
[[nodiscard]] bool Generate(std::string_view filepath, std::string_view configpath);
[[nodiscard]] bool LoadHeader();
[[nodiscard]] bool LoadTableHeaders();
[[nodiscard]] bool LoadTables();
public:
virtual ~TROPUSRLoader() = default;
struct load_result
{
bool discarded_existing;
bool success;
};
[[nodiscard]] load_result Load(std::string_view filepath, std::string_view configpath);
[[nodiscard]] bool Save(std::string_view filepath);
[[nodiscard]] u32 GetTrophiesCount() const;
[[nodiscard]] u32 GetUnlockedTrophiesCount() const;
[[nodiscard]] u32 GetUnlockedPlatinumID(u32 trophy_id, const std::string& config_path);
[[nodiscard]] u32 GetTrophyGrade(u32 id) const;
[[nodiscard]] u32 GetTrophyUnlockState(u32 id) const;
[[nodiscard]] u64 GetTrophyTimestamp(u32 id) const;
[[nodiscard]] bool UnlockTrophy(u32 id, u64 timestamp1, u64 timestamp2);
[[nodiscard]] bool LockTrophy(u32 id);
};

220
rpcs3/Loader/TRP.cpp Normal file
View file

@ -0,0 +1,220 @@
#include "stdafx.h"
#include "Emu/VFS.h"
#include "TRP.h"
#include "Crypto/sha1.h"
#include "util/StrUtil.h"
LOG_CHANNEL(trp_log, "Trophy");
TRPLoader::TRPLoader(const fs::file& f)
: trp_f(f)
{
}
bool TRPLoader::Install(std::string_view dest, bool /*show*/)
{
if (!trp_f)
{
fs::g_tls_error = fs::error::noent;
return false;
}
fs::g_tls_error = {};
const std::string local_path = vfs::get(dest);
const std::string temp = fmt::format(u8"%s.temp%u", local_path, utils::get_unique_tsc());
if (!fs::create_dir(temp))
{
trp_log.error("Failed to create temp dir: '%s' (error=%s)", temp, fs::g_tls_error);
return false;
}
// Save TROPUSR.DAT
if (!fs::copy_file(local_path + "/TROPUSR.DAT", temp + "/TROPUSR.DAT", false))
{
trp_log.error("Failed to copy TROPUSR.DAT from '%s' to '%s' (error=%s)", local_path, temp, fs::g_tls_error);
}
std::vector<char> buffer(65536);
bool success = true;
for (const TRPEntry& entry : m_entries)
{
trp_f.seek(entry.offset);
if (!trp_f.read(buffer, entry.size))
{
trp_log.error("Failed to read TRPEntry at: offset=0x%x, size=0x%x", entry.offset, entry.size);
continue; // ???
}
// Create the file in the temporary directory
const std::string filename = temp + '/' + vfs::escape(entry.name);
success = fs::write_file<true>(filename, fs::create + fs::excl, buffer);
if (!success)
{
trp_log.error("Failed to write file '%s' (error=%s)", filename, fs::g_tls_error);
break;
}
}
if (success)
{
success = fs::remove_all(local_path, true, true);
if (success)
{
// Atomically create trophy data (overwrite existing data)
success = fs::rename(temp, local_path, false);
if (!success)
{
trp_log.error("Failed to move directory '%s' to '%s' (error=%s)", temp, local_path, fs::g_tls_error);
}
}
else
{
trp_log.error("Failed to remove directory '%s' (error=%s)", local_path, fs::g_tls_error);
}
}
if (!success)
{
// Remove temporary directory manually on failure (removed automatically on success)
auto old_error = fs::g_tls_error;
fs::remove_all(temp);
fs::g_tls_error = old_error;
}
return success;
}
bool TRPLoader::LoadHeader(bool show)
{
if (!trp_f)
{
return false;
}
trp_f.seek(0);
if (!trp_f.read(m_header))
{
return false;
}
if (m_header.trp_magic != 0xDCA24D00)
{
return false;
}
if (show)
{
trp_log.notice("TRP version: 0x%x", m_header.trp_version);
}
if (m_header.trp_version >= 2)
{
unsigned char hash[20];
std::vector<u8> file_contents;
trp_f.seek(0);
if (!trp_f.read(file_contents, m_header.trp_file_size))
{
trp_log.notice("Failed verifying checksum");
}
else
{
memset(&(reinterpret_cast<TRPHeader*>(file_contents.data()))->sha1, 0, 20);
sha1(reinterpret_cast<const unsigned char*>(file_contents.data()), m_header.trp_file_size, hash);
if (memcmp(hash, m_header.sha1, 20) != 0)
{
trp_log.error("Invalid checksum of TROPHY.TRP file");
return false;
}
}
trp_f.seek(sizeof(m_header));
}
m_entries.clear();
if (!trp_f.read(m_entries, m_header.trp_files_count))
{
return false;
}
if (show)
{
for (const auto& entry : m_entries)
{
trp_log.notice("TRP entry #%u: %s", &entry - m_entries.data(), entry.name);
}
}
return true;
}
u64 TRPLoader::GetRequiredSpace() const
{
const u64 file_size = m_header.trp_file_size;
const u64 file_element_size = u64{1} * m_header.trp_files_count * m_header.trp_element_size;
return file_size - sizeof(m_header) - file_element_size;
}
bool TRPLoader::ContainsEntry(std::string_view filename)
{
if (filename.size() >= sizeof(TRPEntry::name))
{
return false;
}
for (const TRPEntry& entry : m_entries)
{
if (entry.name == filename)
{
return true;
}
}
return false;
}
void TRPLoader::RemoveEntry(std::string_view filename)
{
if (filename.size() >= sizeof(TRPEntry::name))
{
return;
}
std::vector<TRPEntry>::iterator i = m_entries.begin();
while (i != m_entries.end())
{
if (i->name == filename)
{
i = m_entries.erase(i);
}
else
{
i++;
}
}
}
void TRPLoader::RenameEntry(std::string_view oldname, std::string_view newname)
{
if (oldname.size() >= sizeof(TRPEntry::name) || newname.size() >= sizeof(TRPEntry::name))
{
return;
}
for (TRPEntry& entry : m_entries)
{
if (entry.name == oldname)
{
strcpy_trunc(entry.name, newname);
}
}
}

40
rpcs3/Loader/TRP.h Normal file
View file

@ -0,0 +1,40 @@
#pragma once
struct TRPHeader
{
be_t<u32> trp_magic;
be_t<u32> trp_version;
be_t<u64> trp_file_size;
be_t<u32> trp_files_count;
be_t<u32> trp_element_size;
be_t<u32> trp_dev_flag;
unsigned char sha1[20];
unsigned char padding[16];
};
struct TRPEntry
{
char name[32];
be_t<u64> offset;
be_t<u64> size;
be_t<u32> unknown;
char padding[12];
};
class TRPLoader final
{
const fs::file& trp_f;
TRPHeader m_header{};
std::vector<TRPEntry> m_entries{};
public:
TRPLoader(const fs::file& f);
bool Install(std::string_view dest, bool show = false);
bool LoadHeader(bool show = false);
u64 GetRequiredSpace() const;
bool ContainsEntry(std::string_view filename);
void RemoveEntry(std::string_view filename);
void RenameEntry(std::string_view oldname, std::string_view newname);
};

149
rpcs3/Loader/disc.cpp Normal file
View file

@ -0,0 +1,149 @@
#include "stdafx.h"
#include "disc.h"
#include "PSF.h"
#include "util/logs.hpp"
#include "util/StrUtil.h"
#include "Emu/System.h"
#include "Emu/system_utils.hpp"
LOG_CHANNEL(disc_log, "DISC");
namespace disc
{
disc_type get_disc_type(const std::string& path, std::string& disc_root, std::string& ps3_game_dir)
{
disc_type type = disc_type::unknown;
disc_root.clear();
ps3_game_dir.clear();
if (path.empty())
{
disc_log.error("Can not determine disc type. Path is empty.");
return disc_type::invalid;
}
if (!fs::is_dir(path))
{
disc_log.error("Can not determine disc type. Path not a directory: '%s'", path);
return disc_type::invalid;
}
// Check for PS3 game first.
std::string elf_path;
if (const game_boot_result result = Emulator::GetElfPathFromDir(elf_path, path);
result == game_boot_result::no_errors)
{
// Every error past this point is considered a corrupt disc.
std::string sfb_dir;
const std::string elf_dir = fs::get_parent_dir(elf_path);
Emulator::GetBdvdDir(disc_root, sfb_dir, ps3_game_dir, elf_dir);
if (!fs::is_dir(disc_root) || !fs::is_dir(sfb_dir) || ps3_game_dir.empty())
{
disc_log.error("Not a PS3 disc: invalid folder (bdvd_dir='%s', sfb_dir='%s', game_dir='%s')", disc_root, sfb_dir, ps3_game_dir);
return disc_type::invalid;
}
// Load PARAM.SFO
const std::string sfo_dir = rpcs3::utils::get_sfo_dir_from_game_path(elf_dir + "/../", "");
const psf::registry _psf = psf::load_object(sfo_dir + "/PARAM.SFO");
if (_psf.empty())
{
disc_log.error("Not a PS3 disc: Corrupted PARAM.SFO found! (path='%s/PARAM.SFO')", sfo_dir);
return disc_type::invalid;
}
const std::string cat = std::string(psf::get_string(_psf, "CATEGORY"));
if (cat != "DG")
{
disc_log.error("Not a PS3 disc: Wrong category '%s'.", cat);
return disc_type::invalid;
}
return disc_type::ps3;
}
disc_log.notice("Not a PS3 disc: Elf not found. Looking for SYSTEM.CNF... (path='%s')", path);
std::vector<std::string> lines;
// Try to find SYSTEM.CNF
for (std::string search_dir = path;;)
{
if (fs::file file(search_dir + "/SYSTEM.CNF"); file)
{
disc_root = search_dir + "/";
lines = fmt::split(file.to_string(), {"\n"});
break;
}
std::string parent_dir = fs::get_parent_dir(search_dir);
if (parent_dir.size() == search_dir.size())
{
// Keep looking until root directory is reached
disc_log.error("SYSTEM.CNF not found in path: '%s'", path);
return disc_type::invalid;
}
search_dir = std::move(parent_dir);
}
for (usz i = 0; i < lines.size(); i++)
{
const std::string& line = lines[i];
const usz pos = line.find('=');
if (pos == umax)
{
continue;
}
const std::string key = fmt::trim(line.substr(0, pos));
std::string value;
if (pos != (line.size() - 1))
{
value = fmt::trim(line.substr(pos + 1));
}
if (value.empty() && i != (lines.size() - 1) && line.size() != 1)
{
// Some games have a character on the last line of the file, don't print the error in those cases.
disc_log.warning("Unusual or malformed entry in SYSTEM.CNF ignored: %s", line);
continue;
}
if (key == "BOOT2")
{
disc_log.notice("SYSTEM.CNF - Detected PS2 Disc = %s", value);
type = disc_type::ps2;
}
else if (key == "BOOT")
{
disc_log.notice("SYSTEM.CNF - Detected PSX/PSone Disc = %s", value);
type = disc_type::ps1;
}
else if (key == "VMODE")
{
disc_log.notice("SYSTEM.CNF - Disc region type = %s", value);
}
else if (key == "VER")
{
disc_log.notice("SYSTEM.CNF - Software version = %s", value);
}
}
if (type == disc_type::unknown)
{
disc_log.error("SYSTEM.CNF - Disc is not a PSX/PSone or PS2 game!");
}
return type;
}
} // namespace disc

15
rpcs3/Loader/disc.h Normal file
View file

@ -0,0 +1,15 @@
#pragma once
namespace disc
{
enum class disc_type
{
invalid,
unknown,
ps1,
ps2,
ps3
};
disc_type get_disc_type(const std::string& path, std::string& disc_root, std::string& ps3_game_dir);
} // namespace disc

83
rpcs3/Loader/mself.cpp Normal file
View file

@ -0,0 +1,83 @@
#include "stdafx.h"
#include "util/File.h"
#include "util/logs.hpp"
#include "Emu/VFS.h"
#include "mself.hpp"
LOG_CHANNEL(mself_log, "MSELF");
bool extract_mself(const std::string& file, const std::string& extract_to)
{
fs::file mself(file);
mself_log.notice("Extracting MSELF file '%s' to directory '%s'...", file, extract_to);
if (!mself)
{
mself_log.error("Error opening MSELF file '%s' (%s)", file, fs::g_tls_error);
return false;
}
mself_header hdr{};
if (!mself.read(hdr))
{
mself_log.error("Error reading MSELF header, file is too small. (size=0x%x)", mself.size());
return false;
}
const u64 mself_size = mself.size();
const u32 hdr_count = hdr.get_count(mself_size);
if (!hdr_count)
{
mself_log.error("Provided file is not an MSELF");
return false;
}
std::vector<mself_record> recs(hdr_count);
if (!mself.read(recs))
{
mself_log.error("Error extracting MSELF records");
return false;
}
std::vector<u8> buffer;
for (const mself_record& rec : recs)
{
const std::string name = vfs::escape(rec.name);
const u64 pos = rec.get_pos(mself_size);
if (!pos)
{
mself_log.error("Error extracting %s from MSELF", name);
return false;
}
buffer.resize(rec.size);
mself.seek(pos);
mself.read(buffer.data(), rec.size);
if (!fs::create_path(fs::get_parent_dir(extract_to + name)))
{
mself_log.error("Error creating directory %s (%s)", fs::get_parent_dir(extract_to + name), fs::g_tls_error);
return false;
}
if (!fs::write_file(extract_to + name, fs::rewrite, buffer))
{
mself_log.error("Error creating %s (%s)", extract_to + name, fs::g_tls_error);
return false;
}
mself_log.success("Extracted '%s' to '%s'", name, extract_to + name);
}
mself_log.success("Extraction complete!");
return true;
}

46
rpcs3/Loader/mself.hpp Normal file
View file

@ -0,0 +1,46 @@
#pragma once
#include "util/types.hpp"
#include "util/endian.hpp"
struct mself_record
{
char name[0x20];
be_t<u64> off;
be_t<u64> size;
u8 reserved[0x10];
u64 get_pos(u64 file_size) const
{
// Fast sanity check
if (off < file_size && file_size - off >= size) [[likely]]
return off;
return 0;
}
};
struct mself_header
{
nse_t<u32> magic; // "MSF\x00"
be_t<u32> ver; // 1
be_t<u64> size; // File size
be_t<u32> count; // Number of records
be_t<u32> header_size; // ???
u8 reserved[0x28];
u32 get_count(u64 file_size) const
{
// Fast sanity check
if (magic != "MSF"_u32 || ver != u32{1} || (file_size - sizeof(mself_header)) / sizeof(mself_record) < count || this->size != file_size) [[unlikely]]
return 0;
return count;
}
};
CHECK_SIZE(mself_header, 0x40);
CHECK_SIZE(mself_record, 0x40);
bool extract_mself(const std::string& file, const std::string& extract_to);