mirror of
https://github.com/RPCSX/rpcsx.git
synced 2026-04-04 14:08:37 +00:00
split rpcs3 and hle libraries
merge rpcs3 utilities
This commit is contained in:
parent
b33e2662b6
commit
62ad27d1e2
1233 changed files with 7004 additions and 3819 deletions
31
rpcs3/Loader/ELF.cpp
Normal file
31
rpcs3/Loader/ELF.cpp
Normal 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
550
rpcs3/Loader/ELF.h
Normal 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
431
rpcs3/Loader/PSF.cpp
Normal 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
161
rpcs3/Loader/PSF.h
Normal 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
116
rpcs3/Loader/PUP.cpp
Normal 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
77
rpcs3/Loader/PUP.h
Normal 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
703
rpcs3/Loader/TAR.cpp
Normal 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
63
rpcs3/Loader/TAR.h
Normal 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
394
rpcs3/Loader/TROPUSR.cpp
Normal 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
112
rpcs3/Loader/TROPUSR.h
Normal 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
220
rpcs3/Loader/TRP.cpp
Normal 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
40
rpcs3/Loader/TRP.h
Normal 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
149
rpcs3/Loader/disc.cpp
Normal 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
15
rpcs3/Loader/disc.h
Normal 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
83
rpcs3/Loader/mself.cpp
Normal 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
46
rpcs3/Loader/mself.hpp
Normal 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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue