Rewrite Save Data & Impl Save Data Dialog (#824)

* core: Rewrite PSF parser & add encoder

add .sfo hex pattern to /scripts

* core/fs: allow to mount path as read-only

* common: Add CString wrapper to handle native null-terminated strings

* SaveData: rewrite to implement full functionality

* mock value for SYSTEM_VER

* SavaData: backup features

* SavaData: SaveDataMemory features

* imgui Ref-counted textures

- has a background thread to decode textures

* imgui: rework gamepad navigation

* PSF: fixed psf not using enum class for PSFEntryFmt (was a standard old ugly enum)

- Add null check to CString when itself is used in a nullable field

* SaveDataDialog implementation

- Fix Mounting/Unmounting check of SaveInstance
This commit is contained in:
Vinicius Rangel 2024-09-20 06:34:19 -03:00 committed by GitHub
parent 077f8981a7
commit 0f4bcd8c83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 4919 additions and 1134 deletions

View file

@ -2,61 +2,275 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include "common/assert.h"
#include "common/io_file.h"
#include "common/logging/log.h"
#include "core/file_format/psf.h"
PSF::PSF() = default;
static const std::unordered_map<std::string_view, u32> psf_known_max_sizes = {
{"ACCOUNT_ID", 8}, {"CATEGORY", 4}, {"DETAIL", 1024}, {"FORMAT", 4},
{"MAINTITLE", 128}, {"PARAMS", 1024}, {"SAVEDATA_BLOCKS", 8}, {"SAVEDATA_DIRECTORY", 32},
{"SUBTITLE", 128}, {"TITLE_ID", 12},
};
static inline u32 get_max_size(std::string_view key, u32 default_value) {
if (const auto& v = psf_known_max_sizes.find(key); v != psf_known_max_sizes.end()) {
return v->second;
}
return default_value;
}
PSF::~PSF() = default;
bool PSF::open(const std::string& filepath, const std::vector<u8>& psfBuffer) {
if (!psfBuffer.empty()) {
psf.resize(psfBuffer.size());
psf = psfBuffer;
} else {
Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read);
if (!file.IsOpen()) {
return false;
}
const u64 psfSize = file.GetSize();
psf.resize(psfSize);
file.Seek(0);
file.Read(psf);
file.Close();
bool PSF::Open(const std::filesystem::path& filepath) {
if (std::filesystem::exists(filepath)) {
last_write = std::filesystem::last_write_time(filepath);
}
// Parse file contents
PSFHeader header;
std::memcpy(&header, psf.data(), sizeof(header));
for (u32 i = 0; i < header.index_table_entries; i++) {
PSFEntry entry;
std::memcpy(&entry, &psf[sizeof(PSFHeader) + i * sizeof(PSFEntry)], sizeof(entry));
Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Read);
if (!file.IsOpen()) {
return false;
}
const std::string key = (char*)&psf[header.key_table_offset + entry.key_offset];
if (entry.param_fmt == PSFEntry::Fmt::TextRaw ||
entry.param_fmt == PSFEntry::Fmt::TextNormal) {
map_strings[key] = (char*)&psf[header.data_table_offset + entry.data_offset];
}
if (entry.param_fmt == PSFEntry::Fmt::Integer) {
u32 value;
std::memcpy(&value, &psf[header.data_table_offset + entry.data_offset], sizeof(value));
map_integers[key] = value;
const u64 psfSize = file.GetSize();
std::vector<u8> psf(psfSize);
file.Seek(0);
file.Read(psf);
file.Close();
return Open(psf);
}
bool PSF::Open(const std::vector<u8>& psf_buffer) {
const u8* psf_data = psf_buffer.data();
entry_list.clear();
map_binaries.clear();
map_strings.clear();
map_integers.clear();
// Parse file contents
PSFHeader header{};
std::memcpy(&header, psf_data, sizeof(header));
if (header.magic != PSF_MAGIC) {
LOG_ERROR(Core, "Invalid PSF magic number");
return false;
}
if (header.version != PSF_VERSION_1_1 && header.version != PSF_VERSION_1_0) {
LOG_ERROR(Core, "Unsupported PSF version: 0x{:08x}", header.version);
return false;
}
for (u32 i = 0; i < header.index_table_entries; i++) {
PSFRawEntry raw_entry{};
std::memcpy(&raw_entry, psf_data + sizeof(PSFHeader) + i * sizeof(PSFRawEntry),
sizeof(raw_entry));
Entry& entry = entry_list.emplace_back();
entry.key = std::string{(char*)(psf_data + header.key_table_offset + raw_entry.key_offset)};
entry.param_fmt = static_cast<PSFEntryFmt>(raw_entry.param_fmt.Raw());
entry.max_len = raw_entry.param_max_len;
const u8* data = psf_data + header.data_table_offset + raw_entry.data_offset;
switch (entry.param_fmt) {
case PSFEntryFmt::Binary: {
std::vector<u8> value(raw_entry.param_len);
std::memcpy(value.data(), data, raw_entry.param_len);
map_binaries.emplace(i, std::move(value));
} break;
case PSFEntryFmt::Text: {
std::string c_str{reinterpret_cast<const char*>(data)};
map_strings.emplace(i, std::move(c_str));
} break;
case PSFEntryFmt::Integer: {
ASSERT_MSG(raw_entry.param_len == sizeof(s32), "PSF integer entry size mismatch");
s32 integer = *(s32*)data;
map_integers.emplace(i, integer);
} break;
default:
UNREACHABLE_MSG("Unknown PSF entry format");
}
}
return true;
}
std::string PSF::GetString(const std::string& key) {
if (map_strings.find(key) != map_strings.end()) {
return map_strings.at(key);
bool PSF::Encode(const std::filesystem::path& filepath) const {
Common::FS::IOFile file(filepath, Common::FS::FileAccessMode::Write);
if (!file.IsOpen()) {
return false;
}
return "";
last_write = std::filesystem::file_time_type::clock::now();
const auto psf_buffer = Encode();
return file.Write(psf_buffer) == psf_buffer.size();
}
u32 PSF::GetInteger(const std::string& key) {
if (map_integers.find(key) != map_integers.end()) {
return map_integers.at(key);
}
return -1;
std::vector<u8> PSF::Encode() const {
std::vector<u8> psf_buffer;
Encode(psf_buffer);
return psf_buffer;
}
void PSF::Encode(std::vector<u8>& psf_buffer) const {
psf_buffer.resize(sizeof(PSFHeader) + sizeof(PSFRawEntry) * entry_list.size());
{
auto& header = *(PSFHeader*)psf_buffer.data();
header.magic = PSF_MAGIC;
header.version = PSF_VERSION_1_1;
header.index_table_entries = entry_list.size();
}
const size_t key_table_offset = psf_buffer.size();
((PSFHeader*)psf_buffer.data())->key_table_offset = key_table_offset;
for (size_t i = 0; i < entry_list.size(); i++) {
auto& raw_entry = ((PSFRawEntry*)(psf_buffer.data() + sizeof(PSFHeader)))[i];
const Entry& entry = entry_list[i];
raw_entry.key_offset = psf_buffer.size() - key_table_offset;
raw_entry.param_fmt.FromRaw(static_cast<u16>(entry.param_fmt));
raw_entry.param_max_len = entry.max_len;
std::ranges::copy(entry.key, std::back_inserter(psf_buffer));
psf_buffer.push_back(0); // NULL terminator
}
const size_t data_table_offset = psf_buffer.size();
((PSFHeader*)psf_buffer.data())->data_table_offset = data_table_offset;
for (size_t i = 0; i < entry_list.size(); i++) {
if (psf_buffer.size() % 4 != 0) {
std::ranges::fill_n(std::back_inserter(psf_buffer), 4 - psf_buffer.size() % 4, 0);
}
auto& raw_entry = ((PSFRawEntry*)(psf_buffer.data() + sizeof(PSFHeader)))[i];
const Entry& entry = entry_list[i];
raw_entry.data_offset = psf_buffer.size() - data_table_offset;
s32 additional_padding = s32(raw_entry.param_max_len);
switch (entry.param_fmt) {
case PSFEntryFmt::Binary: {
const auto& value = map_binaries.at(i);
raw_entry.param_len = value.size();
additional_padding -= s32(raw_entry.param_len);
std::ranges::copy(value, std::back_inserter(psf_buffer));
} break;
case PSFEntryFmt::Text: {
const auto& value = map_strings.at(i);
raw_entry.param_len = value.size() + 1;
additional_padding -= s32(raw_entry.param_len);
std::ranges::copy(value, std::back_inserter(psf_buffer));
psf_buffer.push_back(0); // NULL terminator
} break;
case PSFEntryFmt::Integer: {
const auto& value = map_integers.at(i);
raw_entry.param_len = sizeof(s32);
additional_padding -= s32(raw_entry.param_len);
const auto value_bytes = reinterpret_cast<const u8*>(&value);
std::ranges::copy(value_bytes, value_bytes + sizeof(s32),
std::back_inserter(psf_buffer));
} break;
default:
UNREACHABLE_MSG("Unknown PSF entry format");
}
ASSERT_MSG(additional_padding >= 0, "PSF entry max size mismatch");
std::ranges::fill_n(std::back_inserter(psf_buffer), additional_padding, 0);
}
}
std::optional<std::span<const u8>> PSF::GetBinary(std::string_view key) const {
const auto& [it, index] = FindEntry(key);
if (it == entry_list.end()) {
return {};
}
ASSERT(it->param_fmt == PSFEntryFmt::Binary);
return std::span{map_binaries.at(index)};
}
std::optional<std::string_view> PSF::GetString(std::string_view key) const {
const auto& [it, index] = FindEntry(key);
if (it == entry_list.end()) {
return {};
}
ASSERT(it->param_fmt == PSFEntryFmt::Text);
return std::string_view{map_strings.at(index)};
}
std::optional<s32> PSF::GetInteger(std::string_view key) const {
const auto& [it, index] = FindEntry(key);
if (it == entry_list.end()) {
return {};
}
ASSERT(it->param_fmt == PSFEntryFmt::Integer);
return map_integers.at(index);
}
void PSF::AddBinary(std::string key, std::vector<u8> value, bool update) {
auto [it, index] = FindEntry(key);
bool exist = it != entry_list.end();
if (exist && !update) {
LOG_ERROR(Core, "PSF: Tried to add binary key that already exists: {}", key);
return;
}
if (exist) {
ASSERT_MSG(it->param_fmt == PSFEntryFmt::Binary, "PSF: Change format is not supported");
it->max_len = get_max_size(key, value.size());
map_binaries.at(index) = std::move(value);
return;
}
Entry& entry = entry_list.emplace_back();
entry.max_len = get_max_size(key, value.size());
entry.key = std::move(key);
entry.param_fmt = PSFEntryFmt::Binary;
map_binaries.emplace(entry_list.size() - 1, std::move(value));
}
void PSF::AddString(std::string key, std::string value, bool update) {
auto [it, index] = FindEntry(key);
bool exist = it != entry_list.end();
if (exist && !update) {
LOG_ERROR(Core, "PSF: Tried to add string key that already exists: {}", key);
return;
}
if (exist) {
ASSERT_MSG(it->param_fmt == PSFEntryFmt::Text, "PSF: Change format is not supported");
it->max_len = get_max_size(key, value.size() + 1);
map_strings.at(index) = std::move(value);
return;
}
Entry& entry = entry_list.emplace_back();
entry.max_len = get_max_size(key, value.size() + 1);
entry.key = std::move(key);
entry.param_fmt = PSFEntryFmt::Text;
map_strings.emplace(entry_list.size() - 1, std::move(value));
}
void PSF::AddInteger(std::string key, s32 value, bool update) {
auto [it, index] = FindEntry(key);
bool exist = it != entry_list.end();
if (exist && !update) {
LOG_ERROR(Core, "PSF: Tried to add integer key that already exists: {}", key);
return;
}
if (exist) {
ASSERT_MSG(it->param_fmt == PSFEntryFmt::Integer, "PSF: Change format is not supported");
it->max_len = sizeof(s32);
map_integers.at(index) = value;
return;
}
Entry& entry = entry_list.emplace_back();
entry.key = std::move(key);
entry.param_fmt = PSFEntryFmt::Integer;
entry.max_len = sizeof(s32);
map_integers.emplace(entry_list.size() - 1, value);
}
std::pair<std::vector<PSF::Entry>::iterator, size_t> PSF::FindEntry(std::string_view key) {
auto entry =
std::ranges::find_if(entry_list, [&](const auto& entry) { return entry.key == key; });
return {entry, std::distance(entry_list.begin(), entry)};
}
std::pair<std::vector<PSF::Entry>::const_iterator, size_t> PSF::FindEntry(
std::string_view key) const {
auto entry =
std::ranges::find_if(entry_list, [&](const auto& entry) { return entry.key == key; });
return {entry, std::distance(entry_list.begin(), entry)};
}

View file

@ -3,11 +3,18 @@
#pragma once
#include <filesystem>
#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include "common/endian.h"
constexpr u32 PSF_MAGIC = 0x00505346;
constexpr u32 PSF_VERSION_1_1 = 0x00000101;
constexpr u32 PSF_VERSION_1_0 = 0x00000100;
struct PSFHeader {
u32_be magic;
u32_le version;
@ -15,34 +22,72 @@ struct PSFHeader {
u32_le data_table_offset;
u32_le index_table_entries;
};
static_assert(sizeof(PSFHeader) == 0x14);
struct PSFEntry {
enum Fmt : u16 {
TextRaw = 0x0400, // String in UTF-8 format and not NULL terminated
TextNormal = 0x0402, // String in UTF-8 format and NULL terminated
Integer = 0x0404, // Unsigned 32-bit integer
};
struct PSFRawEntry {
u16_le key_offset;
u16_be param_fmt;
u32_le param_len;
u32_le param_max_len;
u32_le data_offset;
};
static_assert(sizeof(PSFRawEntry) == 0x10);
enum class PSFEntryFmt : u16 {
Binary = 0x0004, // Binary data
Text = 0x0204, // String in UTF-8 format and NULL terminated
Integer = 0x0404, // Signed 32-bit integer
};
class PSF {
struct Entry {
std::string key;
PSFEntryFmt param_fmt;
u32 max_len;
};
public:
PSF();
~PSF();
PSF() = default;
~PSF() = default;
bool open(const std::string& filepath, const std::vector<u8>& psfBuffer);
PSF(const PSF& other) = default;
PSF(PSF&& other) noexcept = default;
PSF& operator=(const PSF& other) = default;
PSF& operator=(PSF&& other) noexcept = default;
std::string GetString(const std::string& key);
u32 GetInteger(const std::string& key);
bool Open(const std::filesystem::path& filepath);
bool Open(const std::vector<u8>& psf_buffer);
std::unordered_map<std::string, std::string> map_strings;
std::unordered_map<std::string, u32> map_integers;
[[nodiscard]] std::vector<u8> Encode() const;
void Encode(std::vector<u8>& buf) const;
bool Encode(const std::filesystem::path& filepath) const;
std::optional<std::span<const u8>> GetBinary(std::string_view key) const;
std::optional<std::string_view> GetString(std::string_view key) const;
std::optional<s32> GetInteger(std::string_view key) const;
void AddBinary(std::string key, std::vector<u8> value, bool update = false);
void AddString(std::string key, std::string value, bool update = false);
void AddInteger(std::string key, s32 value, bool update = false);
[[nodiscard]] std::filesystem::file_time_type GetLastWrite() const {
return last_write;
}
[[nodiscard]] const std::vector<Entry>& GetEntries() const {
return entry_list;
}
private:
std::vector<u8> psf;
mutable std::filesystem::file_time_type last_write;
std::vector<Entry> entry_list;
std::unordered_map<size_t, std::vector<u8>> map_binaries;
std::unordered_map<size_t, std::string> map_strings;
std::unordered_map<size_t, s32> map_integers;
[[nodiscard]] std::pair<std::vector<Entry>::iterator, size_t> FindEntry(std::string_view key);
[[nodiscard]] std::pair<std::vector<Entry>::const_iterator, size_t> FindEntry(
std::string_view key) const;
};